Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/Configuration.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/Configuration.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/Configuration.java	(revision 31635)
@@ -0,0 +1,961 @@
+/**
+ *#########################################################################
+ * Configuration
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer;
+
+import java.awt.*;
+import java.io.*;
+import java.lang.ref.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.plaf.*;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+/** This class stores the various configurable settings inside the Gatherer, both during a session, and between sessions in the form of XML. However not all data members are retained during xml serialization. To further improve efficiency, the property-name -> DOM Element pairs are stored in a SoftReferenced Hashtable.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class Configuration
+    extends Hashtable {
+
+    static final public boolean COLLECTION_SPECIFIC = true;
+    static final public boolean GENERAL_SETTING = true;
+
+    static final public int ASSISTANT_MODE = 1;
+    static final public int LIBRARIAN_MODE = 2;
+    static final public int EXPERT_MODE    = 3; 
+
+    /** The string identifying an argument's name attribute. */
+    static final private String ARGUMENT_NAME = "name";
+    /** The name of the general Gatherer configuration file. */
+    static public String CONFIG_XML = "config.xml";
+    static public String FEDORA_CONFIG_PREFIX = "fedora-";
+
+    /** The name of the root element of the subtree containing gatherer configuration options. This is required as the document itself may contain several other subtrees of settings (such as in the case of a '.col' file). */
+    static final private String GATHERER_CONFIG = "GathererConfig";
+    /** The string identifying an argument element. */
+    static final private String GATHERER_CONFIG_ARGUMENT = "Argument";
+
+    static final private String GENERAL_EMAIL_SETTING = "general.email";
+    /** the lang attribute of the other element */
+    static final private String LANGUAGE = "lang";
+    /** The name of a Name Element. */
+    static final private String NAME = "Name";
+    /** The name of the other arguments element. */
+    static final private String OTHER = "Other";
+    /** The name of the general Gatherer configuration template. */
+    static public String TEMPLATE_CONFIG_XML = "xml/config.xml";
+    /** The first of three patterns used during tokenization, this pattern handles a comma separated list. */
+    static final private String TOKENIZER_PATTERN1 = " ,\n\t";
+    /** The second of three patterns used during tokenization, this pattern handles an underscore separated list. */
+    static final private String TOKENIZER_PATTERN2 = "_\n\t";
+    /** The last of three patterns used during tokenization, this pattern handles an comma separated list containing spaces. */
+    static final private String TOKENIZER_PATTERN3 = ",\n\t";
+
+    static public Configuration self = null;
+    static public String gli_user_directory_path = null;
+
+    static public boolean just_updated_config_xml_file = false;
+    /** The path to the Greenstone Suite installation directory. */
+    static public String gsdl_path = "";
+    /** If we are using GLI in Greenstone 3, the path to gsdl3 home (== web dir) */
+    static public String gsdl3_path = "";
+    /** If we are using GLI in Greenstone 3, the path to gsdl3 src directory (== gsdl3 dir)*/
+    static public String gsdl3_src_path = "";
+
+    static public FedoraInfo fedora_info = null;
+    
+    /** The path to the PERL executable, up to and including Perl.exe. */
+    static public String perl_path = "";
+    /** The password for the proxy server indicated above. */
+    static public String proxy_pass = null;
+    /** The username for the proxy server indicated above. */
+    static public String proxy_user = null;
+    /** The screen size of the desktop the Gatherer will be displayed on. */
+    static public Dimension screen_size = Toolkit.getDefaultToolkit().getScreenSize();
+    /** The site name if we are using GS3 */
+    static public String site_name = "";
+    /** The servlet path if we are using GS3 */
+    static public String servlet_path = "";
+    /** Collection level configuration (which in some cases overrides general configuration. */
+    static private Document collection_config;
+    /** The general configuration settings. */
+    static private Document general_config;
+
+    /** The element, from glis config file, that contains the various directory mappings. */
+    static private Element directory_mappings_element;
+
+    static private int cache_hit = 0;
+    static private int cache_miss = 0;
+
+    static public URL gliserver_url = null;
+    static public URL library_url = null;
+    
+    
+    /** Constructor.
+     * @param gsdl_path The path to the Greenstone directory as a <strong>String</strong>.
+     * @param gsdl3_path The path to the Greenstone 3 home (web) directory as a <strong>String</strong>.
+     * @param gsdl3_src_path The path to the Greenstone 3 src directory as a <strong>String</strong>.
+     * @param site_name The name of the Greenstone 3 site currently in use.
+     * @param fedora_info A FedoraInfo object containing the user-provided details for creating
+     * a connection to a Fedora repository.
+     */
+    public Configuration(String gli_user_directory_path, String gsdl_path, String gsdl3_path, String gsdl3_src_path, 
+				 String site_name, FedoraInfo fedora_info) 
+    {
+	super();
+	self = this; // allows Configuration to statically refer to itself in the static methods
+
+	this.gli_user_directory_path = gli_user_directory_path;
+	this.gsdl_path = gsdl_path;
+	this.gsdl3_path = gsdl3_path;
+	this.gsdl3_src_path = gsdl3_src_path;
+	this.site_name = site_name;
+
+	this.fedora_info = fedora_info;
+
+	// Try to load the configuration file from the user specific location
+	String config_xml_file_name = CONFIG_XML;
+
+	if (fedora_info != null && fedora_info.isActive()) {
+	    config_xml_file_name = FEDORA_CONFIG_PREFIX + config_xml_file_name;
+	}
+
+	// If the existing user config.xml file isn't recent enough, backup the old version and update it
+	File config_xml_file = new File(gli_user_directory_path + config_xml_file_name);
+
+	if (config_xml_file != null && config_xml_file.exists()) {
+	    just_updated_config_xml_file = updateUserConfigXMLFileIfNecessary(config_xml_file);
+	    general_config = XMLTools.parseXMLFile(config_xml_file);
+	}
+
+	// Create a new config from the default configuration file from our xml library
+	if (general_config == null) {
+	    general_config = XMLTools.parseXMLFile(TEMPLATE_CONFIG_XML, true);
+	    just_updated_config_xml_file = true;
+	}
+
+	// Determine the Directory Mappings element
+	directory_mappings_element = (Element) XMLTools.getNodeFromNamed(general_config.getDocumentElement(), StaticStrings.DIRECTORY_MAPPINGS_ELEMENT);
+
+	// Re-establish the color settings.
+	updateUI();
+
+	// Read the Greenstone library URL from the config file
+	String library_url_string = getString("general.library_url"+gliPropertyNameSuffix(), true);
+	if (!library_url_string.equals("")) {
+	    try {
+		library_url = new URL(library_url_string);
+	    }
+	    catch (MalformedURLException exception) {
+		DebugStream.printStackTrace(exception);
+	    }
+	}
+
+	// Read the Greenstone gliserver URL from the config file, if using a remote Greenstone
+	if (Gatherer.isGsdlRemote) {
+	    String gliserver_url_string = getString("general.gliserver_url", true);
+	    if (!gliserver_url_string.equals("")) {
+		try {
+		    gliserver_url = new URL(gliserver_url_string);
+		}
+		catch (MalformedURLException exception) {
+		    DebugStream.printStackTrace(exception);
+		}
+	    }
+	}
+	
+	if(gsdl3_path != null) {
+	    prepareForGS3();
+	}
+
+    }
+
+    /** @return the suffix for the the config file's library_url/open_collection propertyname. 
+     * For Fedora and remote GS2 cases, there is no suffix.
+     * However, when dealing with a local Greenstone server, this returns suffix _gs2 or _gs3
+     * so it can be appended to the default library_url/open_collection propertynames.
+     * Having 2 separate library URL properties and open_collection properties for the two 
+     * local versions of Greenstone allows GLI to save the library_url and last opened 
+     * collection for both GS2 and GS3, in case anyone runs them alternatively. (This is useful
+     * when developing and testing across GS versions.) */
+    static public String gliPropertyNameSuffix() {
+	if(Gatherer.isGsdlRemote) {
+	    return ""; // no special suffix
+	} else if(Gatherer.GS3) { // local GS3, including FLI for GS3
+	    return "_gs3";
+	} else { // local GS2, including FLI for GS2
+	    return "_gs2";
+	}
+    }
+
+
+    // Called when (gsdl3_path != null) || (Gatherer.GS3 && Gatherer.isGsdlRemote) 
+    static public void prepareForGS3() {
+	if (site_name == null || site_name.equals("")) {
+	    site_name = getString("general.site_name", true);
+	    servlet_path = getString("general.servlet_path", true);
+	    if (site_name.equals("")) {
+		site_name = "localsite"; // can we assume these??
+		servlet_path = "/library";
+		setString("general.site_name", true, site_name);
+		setString("general.servlet_path", true, servlet_path);
+	    }
+	    
+	} else {
+	    // we set the current one in the config
+	    setString("general.site_name", true, site_name);
+	    if (servlet_path != null && !servlet_path.equals("")) {
+		setString("general.servlet_path", true, servlet_path);
+	    }
+	}
+    }
+
+    static private boolean updateUserConfigXMLFileIfNecessary(File config_xml_file)
+    {
+	// Check if the configuration file actually needs updating by looking at the version numbers
+	Document new_document = XMLTools.parseXMLFile(TEMPLATE_CONFIG_XML, true);
+	Document old_document = XMLTools.parseXMLFile(config_xml_file);
+	if (new_document == null || old_document == null) {
+	    // Something has gone badly wrong, so we can't update the file
+	    return false;
+	}
+
+	String new_version = new_document.getDocumentElement().getAttribute(StaticStrings.VERSION_ATTRIBUTE);
+	String old_version = old_document.getDocumentElement().getAttribute(StaticStrings.VERSION_ATTRIBUTE);
+	if (new_version.equals(old_version)) {
+	    // Don't need to update file
+	    return false;
+	}
+
+	System.err.println("Converting user config.xml from version " + old_version + " to version " + new_version + "...");
+
+	// Build up the new user config.xml file from the template config.xml with the user's preferences added
+	// Make a backup of the old config.xml file first
+	config_xml_file.renameTo(new File(config_xml_file.getAbsolutePath() + ".old"));
+
+	// Check all of the Argument elements in the new config.xml file
+	NodeList new_argument_elements_nodelist = new_document.getElementsByTagName("Argument");
+	NodeList old_argument_elements_nodelist = old_document.getElementsByTagName("Argument");
+	for (int i = 0; i < new_argument_elements_nodelist.getLength(); i++) {
+	    Element new_argument_element = (Element) new_argument_elements_nodelist.item(i);
+	    String new_argument_element_name = new_argument_element.getAttribute("name");
+	    String new_argument_element_value = XMLTools.getElementTextValue(new_argument_element);
+
+	    // Did this Argument have a non-default value set?
+	    for (int j = old_argument_elements_nodelist.getLength() - 1; j >= 0; j--) {
+		Element old_argument_element = (Element) old_argument_elements_nodelist.item(j);
+		String old_argument_element_name = old_argument_element.getAttribute("name");
+
+		// Argument found
+		if (old_argument_element_name.equals(new_argument_element_name)) {
+		  // we don't want to carry over open_collection or library_url
+		  // as these may be invalid
+		  if (!new_argument_element_name.equals("general.open_collection"+Configuration.gliPropertyNameSuffix()) && !new_argument_element_name.equals("general.library_url"+Configuration.gliPropertyNameSuffix())) {
+		    
+		    String old_argument_element_value = XMLTools.getElementTextValue(old_argument_element);
+		    if (!old_argument_element_value.equals(new_argument_element_value)) {
+		      XMLTools.setElementTextValue(new_argument_element, old_argument_element_value);
+		    }
+		  }
+		  
+		  // We don't care about this option any more
+		    old_argument_element.getParentNode().removeChild(old_argument_element);
+		    break;
+		}
+	    }
+	}
+
+	// Are there any old Argument elements left over? Keep these around for old time's sake
+	old_argument_elements_nodelist = old_document.getElementsByTagName("Argument");
+	if (old_argument_elements_nodelist.getLength() > 0) {
+	    NodeList new_gathererconfig_elements_nodelist = new_document.getElementsByTagName("GathererConfig");
+	    if (new_gathererconfig_elements_nodelist.getLength() > 0) {
+		Element new_gathererconfig_element = (Element) new_gathererconfig_elements_nodelist.item(0);
+		new_gathererconfig_element.appendChild(new_document.createComment(" Legacy options "));
+
+		// Add the legacy Arguments to the GathererConfig element
+		for (int i = 0; i < old_argument_elements_nodelist.getLength(); i++) {
+		    Element old_argument_element = (Element) old_argument_elements_nodelist.item(i);
+		    new_gathererconfig_element.appendChild(new_document.importNode(old_argument_element, true));
+		}
+	    }
+	}
+
+	// Keep all the Mapping elements
+	NodeList new_directorymappings_elements_nodelist = new_document.getElementsByTagName("DirectoryMappings");
+	if (new_directorymappings_elements_nodelist.getLength() > 0) {
+	    Element new_directorymappings_element = (Element) new_directorymappings_elements_nodelist.item(0);
+
+	    NodeList old_mapping_elements_nodelist = old_document.getElementsByTagName("Mapping");
+	    for (int i = 0; i < old_mapping_elements_nodelist.getLength(); i++) {
+		Element old_mapping_element = (Element) old_mapping_elements_nodelist.item(i);
+		new_directorymappings_element.appendChild(new_document.importNode(old_mapping_element, true));
+	    }
+	}
+
+	// Check all of the Associations elements in the new config.xml file
+	NodeList new_entry_elements_nodelist = new_document.getElementsByTagName("Entry");
+	NodeList old_entry_elements_nodelist = old_document.getElementsByTagName("Entry");
+	for (int i = 0; i < new_entry_elements_nodelist.getLength(); i++) {
+	    Element new_entry_element = (Element) new_entry_elements_nodelist.item(i);
+	    String new_entry_element_name = new_entry_element.getAttribute("extension");
+	    String new_entry_element_value = XMLTools.getElementTextValue(new_entry_element);
+
+	    // Did this Entry have a non-default value set?
+	    for (int j = old_entry_elements_nodelist.getLength() - 1; j >= 0; j--) {
+		Element old_entry_element = (Element) old_entry_elements_nodelist.item(j);
+		String old_entry_element_name = old_entry_element.getAttribute("extension");
+
+		// Entry found
+		if (old_entry_element_name.equals(new_entry_element_name)) {
+		    String old_entry_element_value = XMLTools.getElementTextValue(old_entry_element);
+		    if (!old_entry_element_value.equals(new_entry_element_value)) {
+			XMLTools.setElementTextValue(new_entry_element, old_entry_element_value);
+		    }
+
+		    // We don't care about this option any more
+		    old_entry_element.getParentNode().removeChild(old_entry_element);
+		    break;
+		}
+	    }
+	}
+
+	// Are there any old Entry elements left over? We want to keep these
+	old_entry_elements_nodelist = old_document.getElementsByTagName("Entry");
+	if (old_entry_elements_nodelist.getLength() > 0) {
+	    NodeList new_associations_elements_nodelist = new_document.getElementsByTagName("Associations");
+	    if (new_associations_elements_nodelist.getLength() > 0) {
+		Element new_associations_element = (Element) new_associations_elements_nodelist.item(0);
+
+		// Add the legacy Entries to the Associations element
+		for (int i = 0; i < old_entry_elements_nodelist.getLength(); i++) {
+		    Element old_entry_element = (Element) old_entry_elements_nodelist.item(i);
+		    new_associations_element.appendChild(new_document.importNode(old_entry_element, true));
+		}
+	    }
+	}
+
+	// Write out the updated config.xml file
+	XMLTools.writeXMLFile(config_xml_file, new_document);
+	return true;
+    }
+
+
+    /** Add a special directory mapping. */
+    static public boolean addDirectoryMapping(String name, File file) {
+	boolean result = false;
+	try {
+	    // Ensure the name isn't already in use.
+	    boolean found = false;
+	    NodeList mappings = directory_mappings_element.getElementsByTagName(StaticStrings.MAPPING_ELEMENT);
+	    for(int i = 0; !found && i < mappings.getLength(); i++) {
+		Element mapping_element = (Element) mappings.item(i);
+		if(mapping_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equalsIgnoreCase(name)) {
+		    found = true;
+		}
+		mapping_element = null;
+	    }
+	    // Otherwise add the mapping.
+	    if(!found) {
+		Element mapping_element = general_config.createElement(StaticStrings.MAPPING_ELEMENT);
+		mapping_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+		mapping_element.setAttribute(StaticStrings.FILE_ATTRIBUTE, file.toString());
+		directory_mappings_element.appendChild(mapping_element);
+		result = true;
+		mapping_element = null;
+	    }
+	    mappings = null;
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	return result;
+    } /** addDirectoryMapping(String name, String file) **/
+
+    /** The default get action retrieves the named property from the desired configuration, and returns a true or false. */
+    static public boolean get(String property, boolean general) {
+	String raw = getString(property, general);
+	return (raw != null && raw.equalsIgnoreCase("true"));
+    }
+
+    /** Retrieve all of the configuration preferences which match a certain string. They are returned as a hash map of property names to String objects. */
+    static public HashMap getAll(String property_pattern, boolean general) {
+	HashMap properties = new HashMap();
+	try {
+	    // Locate the appropriate element
+	    Element document_element = null;
+	    if(general) {
+		document_element = general_config.getDocumentElement();
+	    }
+	    else if(collection_config != null) {
+		document_element = collection_config.getDocumentElement();
+	    }
+	    if(document_element != null) {
+		// Retrieve the Gatherer element
+		Element gatherer_element = (Element) XMLTools.getNodeFromNamed(document_element, GATHERER_CONFIG);
+		NodeList arguments = gatherer_element.getElementsByTagName(GATHERER_CONFIG_ARGUMENT);
+		for(int i = 0; i < arguments.getLength(); i++) {
+		    Element argument_element = (Element) arguments.item(i);
+		    if(argument_element.getAttribute(ARGUMENT_NAME).matches(property_pattern)) {
+			String result = XMLTools.getValue(argument_element);
+			// Store a mapping in the cache. Sometimes we will overwrite an existing value (say for collection and general level workflow options) but the processing overhead of detecting these clashes far exceeds any savings.
+			self.put(argument_element.getAttribute(ARGUMENT_NAME) + general, new SoftReference(argument_element));
+			// Add mapping to the properties we're going to return
+			properties.put(argument_element.getAttribute(ARGUMENT_NAME), result);
+		    }
+		}
+	    }
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	return properties;
+    }
+
+    /** Retrieve the value of the named property as a Rectangle. */
+    static public Rectangle getBounds(String property, boolean general) {
+	Rectangle result = null;
+	try {
+	    String raw = getString(property, general);
+	    // Rectangle is (x, y, width, height)
+	    StringTokenizer tokenizer = new StringTokenizer(raw, TOKENIZER_PATTERN1);
+	    int x = Integer.parseInt(tokenizer.nextToken());
+	    int y = Integer.parseInt(tokenizer.nextToken());
+	    int width = Integer.parseInt(tokenizer.nextToken());
+	    int height = Integer.parseInt(tokenizer.nextToken());
+	    result = new Rectangle(x, y, width, height);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	return result;
+    }
+
+    /** Retrieve the value of the named property as a Color. */
+    static public Color getColor(String property, boolean general) {
+	Color result = Color.white; // Default
+	try {
+	    String raw = getString(property, general);
+	    // Color is a RGB triplet list, comma separated (also remove whitespace)
+	    StringTokenizer tokenizer = new StringTokenizer(raw, TOKENIZER_PATTERN1);
+	    int red = Integer.parseInt(tokenizer.nextToken());
+	    int green = Integer.parseInt(tokenizer.nextToken());
+	    int blue = Integer.parseInt(tokenizer.nextToken());
+	    result = new Color(red, green, blue);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	return result;
+    }
+
+
+    /** Retrieve the special directory mappings associated with this collection.
+     * @return A <strong>HashMap</strong> containing mappings from names to directories.
+     */
+    static public HashMap getDirectoryMappings() {
+	HashMap special_directories = new HashMap();
+	try {
+	    // Ensure the name isn't already in use.
+	    boolean found = false;
+	    NodeList mappings = directory_mappings_element.getElementsByTagName(StaticStrings.MAPPING_ELEMENT);
+	    for(int i = 0; !found && i < mappings.getLength(); i++) {
+		Element mapping_element = (Element) mappings.item(i);
+		String name = mapping_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+		File file = new File(mapping_element.getAttribute(StaticStrings.FILE_ATTRIBUTE));
+		if(file.exists()) {
+			special_directories.put(name, file);
+		}
+		file = null;
+		name = null;
+		mapping_element = null;
+	    }
+	    mappings = null;
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	return special_directories;
+    } /** getDirectoryMappings() */
+
+    /** Retrieve the current users email. These are always stored in the general settings.
+     * @return the email address, if it is set, as a String
+     */
+    static public String getEmail() {
+	String email = getString(GENERAL_EMAIL_SETTING, true);
+	return (email.length() > 0 ? email : null);
+    }
+
+    static public Element getFileAssociations() {
+	NodeList file_association_elements = general_config.getDocumentElement().getElementsByTagName(StaticStrings.ASSOCIATIONS_ELEMENT);
+	return (Element) file_association_elements.item(0);
+    }
+
+    /** Retrieve the value of the named property as a FontUIResource. */
+    static public FontUIResource getFont(String property, boolean general) {
+	FontUIResource result = new FontUIResource("Times New Roman", Font.PLAIN, 10);
+	try {
+	    String raw = getString(property, general);
+	    // Font is a face, style, size triplet.
+	    StringTokenizer tokenizer = new StringTokenizer(raw, TOKENIZER_PATTERN3);
+	    String face = tokenizer.nextToken().trim();
+	    ///ystem.err.println("Face:  " + face);
+	    int style = Font.PLAIN;
+	    String temp = tokenizer.nextToken().toUpperCase().trim();
+	    ///ystem.err.println("Style: " + temp);
+	    if(temp.equals("BOLD")) {
+		style = Font.BOLD;
+	    }
+	    else if(temp.equals("ITALIC")) {
+		style = Font.ITALIC;
+	    }
+	    int size = Integer.parseInt(tokenizer.nextToken().trim());
+	    ///ystem.err.println("Size:  " + size);
+	    result = new FontUIResource(face, style, size);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	return result;
+    }
+
+    /** Retrieve the value of the named property as an integer. */
+    static public int getInt(String property, boolean general) {
+	int result = 0;
+	try {
+	    String raw = getString(property, general);
+	    result = Integer.parseInt(raw);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	return result;
+    }
+
+    /** Retrieves the current interface language two letter code. */
+    static public String getLanguage() {
+	Locale locale = getLocale("general.locale", GENERAL_SETTING);
+	String code = "en"; // Default
+	if(locale != null) {
+	    code = locale.getLanguage();
+	}
+	return code;
+    }
+
+    /** Retrieve the value of the named property as a Locale. */
+    static public Locale getLocale(String property, boolean general) {
+	Locale result = Locale.getDefault();
+	try {
+	    String raw = getString(property, general);
+	    // Locale is a underscore separated code.
+	    StringTokenizer tokenizer = new StringTokenizer(raw, TOKENIZER_PATTERN2);
+	    String language = tokenizer.nextToken();
+	    if(tokenizer.hasMoreTokens()) {
+		String country = tokenizer.nextToken();
+		result = new Locale(language, country);
+	    }
+	    else {
+		result = new Locale(language);
+	    }
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	return result;
+    }
+
+    /** Because modes will soon be an integral part of GLI, they get their own easy to remember methods such as this one to get the mode.
+     * @return an int representing the mode
+     */
+    static public int getMode() {
+	return getInt("general.mode", GENERAL_SETTING);
+    }
+
+    /** Return the current mode as a string for use in title bar etc
+     * @return the mode as a String
+     */
+    static public String getModeAsString() {
+	String result;
+	switch(getInt("general.mode", GENERAL_SETTING)) {
+	case ASSISTANT_MODE:
+	    result = Dictionary.get("Preferences.Mode.Assistant");
+	    break;
+	case EXPERT_MODE:
+	    result = Dictionary.get("Preferences.Mode.Expert");
+	    break;
+	default:
+	    result = Dictionary.get("Preferences.Mode.Librarian");
+	}
+	return result;
+    }
+
+    static public String getPreviewCommand() {
+	return getString("general.preview_program", GENERAL_SETTING);
+    }
+
+    static public String getApplicationTitle() {
+	String gli_title = getString("GLI.Title", GENERAL_SETTING);
+	String title = (gli_title=="") ? Gatherer.PROGRAM_NAME : gli_title;
+
+	return title;
+    }
+
+
+    static public String getServletPath() {
+	return servlet_path;
+    }
+
+    /** Retrieve the value of the named property, and noting whether we consult the general or collection specific configuration. */
+    static public String getString(String property, boolean general) {
+	// Its up to this method to find the appropriate node and retrieve the data itself.
+	String result = "";
+	try {
+	    // First of all we look in the cache to see if we have a match.
+	    SoftReference reference = (SoftReference) self.get(property + general);
+	    if(reference != null) {
+		Element argument_element = (Element) reference.get();
+		if(argument_element != null) {
+		    cache_hit++;
+		    result = XMLTools.getValue(argument_element);
+		}
+	    }
+	    // We may have missed in the cache, or the reference may have been consumed.
+	    if(result.length() == 0) {
+		cache_miss++;
+		// Locate the appropriate element
+		Element document_element = null;
+		if(general) {
+		    document_element = general_config.getDocumentElement();
+		}
+		else if(collection_config != null) {
+		    document_element = collection_config.getDocumentElement();
+		}
+		if(document_element != null) {
+		    // Retrieve the Gatherer element
+		    Element gatherer_element = (Element) XMLTools.getNodeFromNamed(document_element, GATHERER_CONFIG);
+		    NodeList arguments = gatherer_element.getElementsByTagName(GATHERER_CONFIG_ARGUMENT);
+		    for(int i = 0; result.length() == 0 && i < arguments.getLength(); i++) {
+			Element argument_element = (Element) arguments.item(i);
+			if(argument_element.getAttribute(ARGUMENT_NAME).equalsIgnoreCase(property)) {
+			    result = XMLTools.getValue(argument_element);
+			    // Store a mapping in the cache. Sometimes we will overwrite an existing value (say for collection and general level workflow options) but the processing overhead of detecting these clashes far exceeds any savings.
+			    self.put(property + general, new SoftReference(argument_element));
+			}
+		    }
+		}
+	    }
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	// If we still have no result, and the search was made in the collection configuration, retrieve the general one instead.
+	if(result.length() == 0 && !general) {
+	    result = getString(property, true);
+	}
+	return result;
+    }
+
+    static public String getGS3ScriptPath() {
+	return gsdl3_src_path + "bin" + File.separator + "script" + File.separator;
+    }
+
+    /** @return path to GS3/bin */
+    static public String getGS3BinPath() {
+	return gsdl3_src_path + "bin" + File.separator;
+    }
+
+    /** @return path to GS3/bin/OS, where OS is windows, darwin or linux */
+    static public String getGS3BinOSPath() {
+	return getGS3BinPath() + Utility.getOSdirName() + File.separator;
+    }
+
+    /** Remove a previously defined special directory mapping.
+     * @param name The name of the mapping to remove as a <strong>String</strong>.
+     * @return The <strong>File</strong> of the mapping removed.
+     */
+    static public File removeDirectoryMapping(String name) {
+	File file = null;
+	try {
+	    // Ensure the name isn't already in use.
+	    boolean found = false;
+	    NodeList mappings = directory_mappings_element.getElementsByTagName(StaticStrings.MAPPING_ELEMENT);
+	    for(int i = 0; !found && i < mappings.getLength(); i++) {
+		Element mapping_element = (Element) mappings.item(i);
+		if(mapping_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equalsIgnoreCase(name)) {
+		    file = new File(XMLTools.getValue(mapping_element));
+		    directory_mappings_element.removeChild(mapping_element);
+		    found = true;
+		}
+		mapping_element = null;
+	    }
+	    mappings = null;
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	return file;
+    } /** removeDirectoryMapping(String name) */
+
+    /** Export the general configuration to file. */
+    static public void save() {
+	///ystem.err.println("Hits " + cache_hit + " vs Misses " + cache_miss);
+	// We first try exporting to a user specific place
+	File user_config_xml = null;
+	String config_xml_name = CONFIG_XML;
+
+	if (fedora_info != null && fedora_info.isActive()) {
+	    config_xml_name = FEDORA_CONFIG_PREFIX + config_xml_name;
+	}
+
+	try {
+	    user_config_xml = new File(gli_user_directory_path + config_xml_name);
+	    ///ystem.err.println("Trying to save to: " + user_config_xml.getAbsolutePath());
+	    ///ystem.err.println("Writing.");
+	    user_config_xml.getParentFile().mkdirs();
+	    XMLTools.writeXMLFile(new File(user_config_xml.getAbsolutePath()), general_config);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	    user_config_xml = null;
+	}
+    }
+
+    /** Set the named property, from the specified configuration, using the given boolean value. */
+    static public void set(String property, boolean general, boolean value) {
+	if(property.startsWith("workflow")) {
+	    DebugStream.println("Set property: " + property + ", general=" + general + ", value=" + value);
+	}
+	setString(property, general, (value ? "true" : "false"));
+    }
+
+    /** Set the collection configuration. */
+    static public void setCollectionConfiguration(Document collection_config_arg) {
+	// clear the cached values
+	self.clear();
+	collection_config = collection_config_arg;
+	updateUI();
+	///atherer.println("Collection configuration set.");
+    }
+
+    /** Set the named property, from the specified configuration, using the given Rectangle value. */
+    static public void setBounds(String property, boolean general, Rectangle value) {
+	StringBuffer text = new StringBuffer("");
+	text.append(value.x);
+	text.append(", ");
+	text.append(value.y);
+	text.append(", ");
+	text.append(value.width);
+	text.append(", ");
+	text.append(value.height);
+	setString(property, general, text.toString());
+    }
+
+    /** Set the named property, from the specified configuration, using the given Color value. */
+    static public void setColor(String property, boolean general, Color value) {
+	StringBuffer text = new StringBuffer("");
+	text.append(value.getRed());
+	text.append(", ");
+	text.append(value.getGreen());
+	text.append(", ");
+	text.append(value.getBlue());
+	setString(property, general, text.toString());
+    }
+
+    /** Establish the current users email.
+     * @param email the email as a String
+     */
+    static public void setEmail(String email) {
+	setString(GENERAL_EMAIL_SETTING, true, email);
+    }
+
+    /** Set the named property, from the specified configuration, using the given Font value. */
+    static public void setFont(String property, boolean general, Font value) {
+	StringBuffer text = new StringBuffer("");
+	text.append(value.getName());
+	text.append(", ");
+	switch(value.getStyle()) {
+	case Font.BOLD:
+	    text.append("BOLD");
+	    break;
+	case Font.ITALIC:
+	    text.append("ITALIC");
+	    break;
+	default:
+	    text.append("PLAIN");
+	}
+	text.append(", ");
+	text.append(value.getSize());
+	setString(property, general, text.toString());
+    }
+
+    /** Set the named property, from the specified configuration, using the given integer value. */
+    static public void setInt(String property, boolean general, int value) {
+        setString(property, general, String.valueOf(value));
+    } 
+
+    /** Set the named property, from the specified configuration, using the given Locale value. */
+    static public void setLocale(String property, boolean general, Locale value) {
+	StringBuffer text = new StringBuffer("");
+	text.append(value.getLanguage());
+	String country = value.getCountry();
+	if(country != null && country.length() > 0) {
+	    text.append("_");
+	    text.append(country);
+	}
+	country = null;
+	setString(property, general, text.toString());
+    }
+
+    /** Because modes will soon be an integral part of GLI, they get their own easy to remember methods such as this one to set the mode.
+     * @param value the new value for mode
+     */
+    static public void setMode(int value) {
+	setInt("general.mode", GENERAL_SETTING, value);
+    }
+
+    static public void setPreviewCommand(String value) {
+	setString("general.preview_program", GENERAL_SETTING, value);
+    }
+
+    static public void setSiteAndServlet(String site, String servlet) {
+	site_name = site;
+	servlet_path = servlet;
+	setString("general.site_name", GENERAL_SETTING, site);
+	setString("general.servlet_path", GENERAL_SETTING, servlet);
+	
+    }
+    /** Sets the value of the named property argument using the given string. */
+    static public void setString(String property, boolean general, String value) {
+	DebugStream.println("Set configuration property: " + property + " = " + value + (general ? "" : " [Collection]"));
+	try {
+	    Document document = general_config;
+	    if(!general && collection_config != null) {
+		document = collection_config;
+	    }
+	    if(document != null) {
+		Element argument_element = null;
+		// Try to retrieve from cache
+		SoftReference reference = (SoftReference) self.get(property + general);
+		if(reference != null) {
+		    argument_element = (Element) reference.get();
+		}
+		if(argument_element == null) {
+		    Element document_element = document.getDocumentElement();
+		    Element gatherer_element = (Element) XMLTools.getNodeFromNamed(document_element, GATHERER_CONFIG);
+		    NodeList arguments = document_element.getElementsByTagName(GATHERER_CONFIG_ARGUMENT);
+		    boolean found = false;
+		    for(int i = 0; argument_element == null && i < arguments.getLength(); i++) {
+			Element possible_element = (Element) arguments.item(i);
+			if(possible_element.getAttribute(ARGUMENT_NAME).equalsIgnoreCase(property)) {
+			    argument_element = possible_element;
+			}
+		    }
+		    // If argument element is still null, create it in the target document.
+		    if(argument_element == null) {
+			argument_element = document.createElement(GATHERER_CONFIG_ARGUMENT);
+			argument_element.setAttribute(ARGUMENT_NAME, property);
+			gatherer_element.appendChild(argument_element);
+		    }
+		    // Update cache
+		    self.put(property + general, new SoftReference(argument_element));
+
+		}
+		if(value == null) {
+		    value = "";
+		}
+		// Now remove any current text node children.
+		NodeList children = argument_element.getChildNodes();
+		for(int i = 0; i < children.getLength(); i++) {
+		    argument_element.removeChild(children.item(i));
+		}
+		// Add a new text node child with the new value
+		argument_element.appendChild(document.createTextNode(value));
+	    }
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+
+    static private void updateUI() {
+	// Buttons
+	UIManager.put("Button.select", new ColorUIResource(getColor("coloring.button_selected_background", false)));
+	UIManager.put("Button.background", new ColorUIResource(getColor("coloring.button_background", false)));
+	UIManager.put("Button.foreground", new ColorUIResource(getColor("coloring.button_foreground", false)));
+
+	UIManager.put("ToggleButton.background", new ColorUIResource(getColor("coloring.button_background", false)));
+	UIManager.put("ToggleButton.foreground", new ColorUIResource(getColor("coloring.button_foreground", false)));
+	UIManager.put("ToggleButton.select", new ColorUIResource(getColor("coloring.button_selected_background", false)));
+
+	// All the things with a lovely Collection green background
+	UIManager.put("OptionPane.background", new ColorUIResource(getColor("coloring.collection_heading_background", false)));
+	UIManager.put("Panel.background", new ColorUIResource(getColor("coloring.collection_heading_background", false)));
+	UIManager.put("Label.background", new ColorUIResource(getColor("coloring.collection_heading_background", false)));
+	UIManager.put("TabbedPane.background", new ColorUIResource(getColor("coloring.collection_heading_background", false)));
+	UIManager.put("SplitPane.background", new ColorUIResource(getColor("coloring.collection_heading_background", false)));
+	UIManager.put("CheckBox.background", new ColorUIResource(getColor("coloring.collection_heading_background", false)));
+
+
+	// Editable coloring
+	UIManager.put("ComboBox.background", new ColorUIResource(getColor("coloring.collection_tree_background", false))); // Indicate clickable
+	UIManager.put("Tree.background", new ColorUIResource(getColor("coloring.collection_tree_background", false)));
+	UIManager.put("Tree.textBackground", new ColorUIResource(getColor("coloring.collection_tree_background", false)));
+	UIManager.put("ProgressBar.background", new ColorUIResource(getColor("coloring.collection_tree_background", false)));
+	UIManager.put("TextArea.background", new ColorUIResource(getColor("coloring.collection_tree_background", false)));
+	UIManager.put("TextField.background", new ColorUIResource(getColor("coloring.editable_background", false)));
+	UIManager.put("Table.background", new ColorUIResource(getColor("coloring.collection_tree_background", false)));
+	UIManager.put("List.background", new ColorUIResource(getColor("coloring.collection_tree_background", false)));
+	UIManager.put("RadioButton.background", new ColorUIResource(getColor("coloring.collection_tree_background", false)));
+
+	// Selection color
+	UIManager.put("TabbedPane.selected", new ColorUIResource(getColor("coloring.collection_selection_background", false)));
+	UIManager.put("Tree.selectionBackground", new ColorUIResource(getColor("coloring.collection_selection_background", false)));
+	UIManager.put("ComboBox.selectionBackground", new ColorUIResource(getColor("coloring.collection_selection_background", false)));
+	UIManager.put("ProgressBar.selectionBackground", new ColorUIResource(getColor("coloring.collection_selection_background", false)));
+	UIManager.put("TextArea.selectionBackground", new ColorUIResource(getColor("coloring.collection_selection_background", false)));
+	UIManager.put("TextField.selectionBackground", new ColorUIResource(getColor("coloring.collection_selection_background", false)));
+	UIManager.put("List.selectionBackground", new ColorUIResource(getColor("coloring.collection_selection_background", false)));
+
+	// Scroll bar stuff
+	UIManager.put("ScrollBar.background", new ColorUIResource(getColor("coloring.scrollbar_background", false)));
+	UIManager.put("ScrollBar.thumb", new ColorUIResource(getColor("coloring.scrollbar_foreground", false)));
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/DebugStream.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/DebugStream.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/DebugStream.java	(revision 31635)
@@ -0,0 +1,120 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer;
+
+
+import java.io.*;
+import java.util.*;
+
+
+public class DebugStream
+{
+    static private boolean debugging_enabled = false;
+    static private PrintStream debug_stream = null;
+
+
+    static public void closeDebugStream()
+    {
+	if (debug_stream != null) {
+	    try {
+		debug_stream.flush();
+		debug_stream.close();
+	    }
+	    catch (Exception exception) {
+		exception.printStackTrace();
+	    }
+	}
+    }
+
+
+    static public void enableDebugging()
+    {
+	debugging_enabled = true;
+    }
+
+
+    static public boolean isDebuggingEnabled()
+    {
+	return debugging_enabled;
+    }
+
+
+    static synchronized public void print(Properties properties)
+    {
+	if (debugging_enabled) {
+	    if (debug_stream != null) {
+		properties.list(debug_stream);
+	    }
+	    properties.list(System.err);
+	}
+    }
+
+
+    /** Print a message to the debug stream. */
+    static synchronized public void print(String message)
+    {
+	if (debugging_enabled) {
+	    if (debug_stream != null) {
+		debug_stream.print(message);
+	    }
+	    System.err.print(message);
+	}
+    }
+
+
+    /** Print a message to the debug stream. */
+    static synchronized public void println(String message)
+    {
+	if (debugging_enabled) {
+	    if (debug_stream != null) {
+		debug_stream.println(message);
+	    }
+	    System.err.println(message);
+	}
+    }
+
+
+    /** Print a stack trace to the debug stream, and always to STDERR. */
+    static synchronized public void printStackTrace(Exception exception)
+    {
+	if (debugging_enabled && debug_stream != null) {
+	    exception.printStackTrace(debug_stream);
+	}
+	exception.printStackTrace();
+    }
+
+
+    static public void setDebugFile(String debug_file_path)
+    {
+	try {
+	    debug_stream = new PrintStream(new FileOutputStream(debug_file_path));
+	}
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/Dictionary.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/Dictionary.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/Dictionary.java	(revision 31635)
@@ -0,0 +1,210 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer;
+
+import java.awt.ComponentOrientation;
+import javax.swing.plaf.FontUIResource;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.ResourceBundle;
+
+
+/** Extends the ResourceBundle class to allow for the automatic insertion of arguments.<BR>
+ * <BR>
+ * Property files usable by this class have the Backus-Naur form: <BR>
+ * <BR>
+ * FileLine ::= Comment | Mapping <BR>
+ * Comment  ::= '#' SString <BR>
+ * Mapping  ::= NZString ':' SString ( Argument SString )* <BR>
+ * NZString ::= ( Char | Int ) SString <BR>
+ * Argument ::= '{' Int '}' <BR>
+ * SString  ::= String . ['"','#',...] -> ['\"','\#',...] <BR>
+ * <BR>
+ * In order to add a new dictionary Locale, simply copy the existing dictionary.properties files, replace the values (Strings after the ':') with the new language specific ones being careful to maintain formatting and Gatherer placed arguments, then save the new dictionary as: <br>
+ * <BR>
+ * dictionary_<I>locale</I>.properties<BR>
+ * <BR>
+ * where locale is made of two two-letter codes seperated by an underscore. The first code is in lower-case and defines the language. The second is in upper-case and defines the country. For example the default dictionary could also correctly be called:
+ * <BR>
+ * dictionary_en_NZ.properties<BR>
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class Dictionary
+    extends HashMap {
+
+    /** The font used when displaying various html text. */
+    static private FontUIResource font = null;
+    /** A reference to remind us of the current locale. */
+    static private Locale locale = null;
+    /** The ResourceBundle which contains the raw key-value mappings.
+	Loaded from a file named "dictionary<I>locale</I>.properties. */
+    static private ResourceBundle dictionary = null;
+    /** language orientation property */
+    static private ComponentOrientation orientation = null;
+       
+    public Dictionary(Locale locale, FontUIResource font)
+    {
+	super();
+
+	// Initialize
+	this.font = font;
+	this.locale = ((locale == null) ? Locale.getDefault() : locale);
+	this.dictionary = ResourceBundle.getBundle("dictionary", this.locale);
+        if (this.get("Component.Orientation").equals("RTL")){
+            this.orientation = ComponentOrientation.RIGHT_TO_LEFT;
+        } else {
+            this.orientation = ComponentOrientation.LEFT_TO_RIGHT;
+        }      
+        
+    }
+    
+    /** Retrieve the two letter code of the current language we are using, according to the stored locale.
+     * @return A <strong>String</strong> containing the two letter ISO639 language code.
+     */
+    static public String getLanguage()
+    {
+	return locale.getLanguage();
+    }
+    /** returns the Components Orientation for use with right to left languages
+     * @return A <strong>ComponentOrientation</strong> with the current 
+     dictionary's language orientation.
+     * by  Amin Hejazi 
+    */
+    static public ComponentOrientation getOrientation()
+    {
+       return orientation;
+    }
+    
+    static public String get(String key)
+    {
+	return get(key, (String[]) null);
+    }
+
+    static public String get(String key, String arg)
+    {
+	String[] args = new String[1];
+	args[0] = arg;
+	return get(key, args);
+    }
+
+
+    /** Used to retrieve a property value from the Locale specific ResourceBundle, based upon the key and arguments supplied. If the key cannot be found or if some other part of the call fails a default (English) error message is returned. <BR>
+     * Here the get recieves a second argument which is an array of Strings used to populate argument fields, denoted {<I>n</I>}, within the value String returned.
+     * @param key A <strong>String</strong> which is mapped to a initial String within the ResourceBundle.
+     * @param args A <strong>String[]</strong> used to populate argument fields within the complete String.
+     * @return A <strong>String</strong> which has been referenced by the key String and that either contains no argument fields, or has had the argument fields automatiically populated with formatting Strings of with argument String provided in the get call.
+     */
+    static public String get(String key, String[] args)
+    {
+	try {
+	  String initial_raw = dictionary.getString(key).trim();
+
+	    // Convert into Unicode
+	    String initial = "";
+	    try {
+		// This "ISO-8859-1" looks out of place, but it is very important.
+		// It is essential to call getBytes with an 8-bit encoding, otherwise
+		// Java kindly deems some characters "undisplayable", and replaces
+		// them with question marks. This is NOT good.
+		initial = new String(initial_raw.getBytes("ISO-8859-1"), "UTF-8");
+	    }
+	    catch (Exception exception) {
+		DebugStream.printStackTrace(exception);
+		return initial_raw;
+	    }
+
+	    // Remove any comments from the string
+	    if (initial.indexOf("#") != -1) {
+		initial = initial.substring(0, initial.indexOf("#"));
+		initial = initial.trim();
+	    }
+
+	    // If the string contains arguments we have to insert them.
+	    StringBuffer complete = new StringBuffer();
+
+	    // While we still have initial string left.
+	    while (initial.length() > 0 && initial.indexOf('{') != -1 && initial.indexOf('}') != -1) {
+		// Remove preamble
+		int opening = initial.indexOf('{');
+		int closing = initial.indexOf('}');
+		int comment_mark = initial.indexOf('-', opening); // May not exist
+		if (comment_mark > closing) { // May also be detecting a later comment
+		    comment_mark = -1;
+		}
+		complete.append(initial.substring(0, opening));
+		
+		// Parse arg_num
+		String arg_str = null;
+		if (comment_mark != -1) {
+		    arg_str = initial.substring(opening + 1, comment_mark);
+		}
+		else {
+		    arg_str = initial.substring(opening + 1, closing);
+		}
+
+		if (closing + 1 < initial.length()) {
+		    initial = initial.substring(closing + 1);
+		}
+		else {
+		    initial = "";
+		}
+
+		// Insert argument
+		if (arg_str.equals("FONT")) {
+		    complete.append((font != null ? font.getFontName() : "Arial"));
+		}
+		else {
+		    int arg_num = Integer.parseInt(arg_str);
+		    if (args != null && 0 <= arg_num && arg_num < args.length) {
+			complete.append(args[arg_num]);
+		    }
+		}
+	    }
+	    complete.append(initial);
+	    
+	    return complete.toString();
+	}
+	catch (Exception exception) {
+	    System.err.println("Missing value for key: " + key);
+	    // DebugStream.printStackTrace(e);
+	    return key;
+	}
+    }
+
+}
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/FedoraInfo.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/FedoraInfo.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/FedoraInfo.java	(revision 31635)
@@ -0,0 +1,177 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson and David Bainbridge, 
+ *         Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer;
+
+import java.io.*;
+import org.greenstone.gatherer.util.StaticStrings; 
+import org.greenstone.gatherer.util.Utility;
+
+public class FedoraInfo 
+{
+    public static final int minimumSupportedMajorVersion = 2;
+
+    private boolean active = false;
+    private String home = null;
+    private String version = null;
+
+    private String hostname = null;
+    private String port     = null;
+    private String username = null;
+    private String password = null;
+    private String protocol = null;
+
+    public FedoraInfo() 
+    {	
+	this("localhost", "8080", "fedoraAdmin", "fedoraAdmin", "http");
+    }
+
+    public FedoraInfo(String hostname, String port, String username, String password, String protocol) 
+    {
+	this.hostname = hostname;
+	this.port     = port;
+	this.username = username;
+	this.password = password;
+	this.protocol = protocol;
+    }
+
+    // Called when we are attempting to run fedora
+    public void setActive(boolean active) {
+	this.active = active; 
+    }
+
+    // Called when an environment variable was wrongly set for running Fedora
+    public void deactivate(String errorMessage) {
+	this.active = false;
+	// display an errormessage and exit cleanly
+	System.err.println(errorMessage);
+	Gatherer.exit();
+    }
+
+    public boolean isActive()
+    {
+	return active;
+    }
+
+    public void setHome(String home)
+    {
+	this.home = home;
+    }
+
+
+    public String getHome()
+    {
+	return home;
+    }
+
+    public void setVersion(String version)
+    {
+	String firstChar = version.substring(0, 1);
+	if(!Character.isDigit(version.charAt(0))) { // version is illegal if it does not begin with a number
+	    deactivate("Incorrect format for major version: " + version + ". The Fedora version must start with a number.");
+	}
+	else if(Integer.parseInt(firstChar) < minimumSupportedMajorVersion) { 
+	    // major version number of Fedora must be more than this
+	    deactivate("The major version number of Fedora must be at least " +  minimumSupportedMajorVersion + ".");
+	}
+	else { // acceptable
+	    this.version = version;
+	} 
+    }
+
+
+    public String getVersion()
+    {
+	return version;
+    }
+
+    public void setHostname(String hostname)
+    {
+	this.hostname = hostname;
+    }
+
+    public String getHostname()
+    {
+	return hostname;
+    }
+
+
+    public void setPort(String port)
+    {
+	this.port = port;
+    }
+
+    public String getPort()
+    {
+	return port;
+    }
+
+
+    public void setUsername(String username)
+    {
+	this.username = username;
+    }
+
+    public String getUsername()
+    {
+	return username;
+    }
+
+
+    public void setPassword(String password)
+    {
+	this.password = password;
+    }
+
+    public String getPassword()
+    {
+	return password;
+    }
+
+
+    public void setProtocol(String protocol)
+    {
+	this.protocol = protocol;
+    }
+
+    public String getProtocol()
+    {
+	return protocol;
+    }
+
+    public String getBaseURL() 
+    {
+	return this.protocol+"://"+this.hostname+":"+this.port+"/";
+    }
+    
+    // The library preview address for Fedora
+    public String getLibraryURL() 
+    {
+	return this.getBaseURL() + "fedora/search";
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GAuthenticator.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GAuthenticator.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GAuthenticator.java	(revision 31635)
@@ -0,0 +1,230 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */ 
+package org.greenstone.gatherer;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+
+
+import org.greenstone.gatherer.gui.GLIButton;
+
+
+/** Provides a graphic authenticator for network password requests.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class GAuthenticator
+    extends Authenticator
+{
+    static public Hashtable authentications = new Hashtable();
+
+    /** Indicates if this authentication prompt been cancelled, and if so rolls-back authentication. */
+    private boolean authentication_cancelled = false;
+    /** The button used to cancel a prompt. */
+    private JButton cancel_button = null;
+    /** The button used to submit the login/password. */
+    private JButton ok_button = null;
+    /** A reference to the dialog prompt created so inner classes can dispose of it. */
+    private JDialog dialog = null;
+    /** The password is a special starred out password field. */
+    private JPasswordField password = null;
+    /** The default size of this dialog. */
+    static final private Dimension SIZE = new Dimension(470,160);
+    
+    /** For Wget downloads, we want to avoid the second automatic request for proxy authentication
+     * But the settings for that specific case will otherwise interfere with how this GAuthenticator 
+     * is to function in the usual situation. For this reason, we use two modes.*/
+    static final public int REGULAR = 0;
+    static final public int DOWNLOAD = 1;
+    static private int operationMode = REGULAR; 
+
+    public static void setMode(int mode) {
+	operationMode = mode;
+    }
+    
+    /** Constructor. */
+    public GAuthenticator() {
+    }
+
+    /** Prompt the user for authentication using a pretty dialog box.
+     * @return A <strong>PasswordAuthentication</strong> object containing the login and password valuees the user has submitted.
+     * @see org.greenstone.gatherer.GAuthenticator.AuthenticationActionListener
+     * @see org.greenstone.gatherer.GAuthenticator.RequestFocusListener
+     */
+    protected PasswordAuthentication getPasswordAuthentication(String username_str, String password_str) {
+	if(operationMode == DOWNLOAD) { // special handling of proxy authentication popup for Wget downloads
+	    
+	    // Don't prompt if the details were already saved for the same host and port. This is necessary
+	    // when running wget because wget requires proxy authentication. And without the following, the
+	    // regular proxy authentication would also popup a dialog requesting the same information.
+	    
+	    String key = getRequestingHost() + ":" + getRequestingPort();
+	    String value = (String)authentications.get(key);
+	    
+	    if(value != null) { // authentication for this host and port was already stored
+		// Arguments may be null. If so, retrieve them from the stored value
+		if(username_str == null) {
+		    username_str = value.substring(0, value.indexOf("@"));
+		} 
+		if(password_str == null) {
+		    password_str = value.substring(value.indexOf("@") + 1);
+		}
+		operationMode = REGULAR; // reset the state of the Authenticator to normal mode
+		return new PasswordAuthentication(username_str, password_str.toCharArray());
+	    }
+	}
+	
+	// Component definition.
+	dialog = new JDialog (Gatherer.g_man, Dictionary.get("GAuthenticator.Title"), true);
+	dialog.setModal(true);
+	dialog.setSize(SIZE);
+	JPanel content_pane = (JPanel) dialog.getContentPane();
+	JLabel title_label = new JLabel(getMessageString());
+
+	JPanel user_panel = new JPanel();
+	JLabel username_label = new JLabel(Dictionary.get("GAuthenticator.Username"));
+	JTextField username = new JTextField();
+	username.setToolTipText(Dictionary.get("GAuthenticator.Username_Tooltip"));
+	if (username_str != null) {
+	    username.setText(username_str);
+	}
+
+	JPanel password_panel = new JPanel();
+	JLabel password_label = new JLabel(Dictionary.get("GAuthenticator.Password"));
+	password = new JPasswordField();
+	password.setEchoChar('*');
+	password.setToolTipText(Dictionary.get("GAuthenticator.Password_Tooltip"));
+	if (password_str != null) {
+	    password.setText(password_str);
+	}
+
+	JPanel button_panel = new JPanel();
+	ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+	
+	// Connection
+	cancel_button.addActionListener(new AuthenticationActionListener(true));
+	ok_button.addActionListener(new AuthenticationActionListener(false));
+	password.addActionListener(new AuthenticationActionListener(false));
+	username.addActionListener(new RequestFocusListener(password));
+
+	// Layout
+	user_panel.setLayout(new GridLayout(1,2));
+	user_panel.add(username_label);
+	user_panel.add(username);
+
+	password_panel.setLayout(new GridLayout(1,2));
+	password_panel.add(password_label);
+	password_panel.add(password);
+
+	button_panel.setLayout(new GridLayout(1,2));
+	button_panel.add(ok_button);
+	button_panel.add(cancel_button);
+
+	content_pane.setLayout(new GridLayout(4,1,0,2));
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.add(title_label);
+	content_pane.add(user_panel);
+	content_pane.add(password_panel);
+	content_pane.add(button_panel);
+
+	// Position the window
+	Dimension screen_size = Toolkit.getDefaultToolkit().getScreenSize();
+	dialog.setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	dialog.setVisible(true);
+	if(!authentication_cancelled) {
+	    // Store the authentication
+	    authentications.put(getRequestingHost() + ":" + getRequestingPort(), username.getText() + "@" + new String(password.getPassword()));
+	    return new PasswordAuthentication(username.getText(), password.getPassword());
+	} else {
+	    return null;
+	}
+    }
+
+    protected PasswordAuthentication getPasswordAuthentication() {
+	return getPasswordAuthentication(null,null);
+    }
+
+    /** This is defined so it can be overridden by subclasses (getRequestingPrompt is final). */
+    protected String getMessageString()
+    {
+	return getRequestingPrompt();
+    }
+
+
+    /** Detects actions upon any control that attempt to submit the current details for authentication. */
+    private class AuthenticationActionListener
+	implements ActionListener {
+	/** <i>true</i> if this authentication action cancels the authentication, <i>false</i> otherwise. */
+	private boolean cancel_action = false;
+	/** Constructor.
+	 * @param cancel_action <i>true</i> if this authentication action cancels the authentication, <i>false</i> otherwise.
+	 */
+	public AuthenticationActionListener(boolean cancel_action) {
+	    this.cancel_action = cancel_action;
+	}
+	/** Any implementation of an ActionListener must include this method so that we can be informed when an action has been performed on our registered controls, allowing us to dispose of the authentication dialog after determining if this is a submit action or a cancel one.
+	 * @param event An <strong>ActionEvent</strong> with information about the event that fired this method.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    authentication_cancelled = cancel_action;
+	    dialog.dispose();
+	}
+    }
+
+    /** This listener detects actions on registered controls, and when they occur ensures the focus is moved to some targetted component. */
+    private class RequestFocusListener
+	implements ActionListener {
+	/*The <strong>Component</strong> you wish to gain focus when an action is performed on a registered control. */
+	private Component target = null;
+	/** Constructor.
+	 * @param target The <strong>Component</strong> you wish to gain focus when an action is performed on a registered control.
+	 */
+	public RequestFocusListener(Component target) {
+	    this.target = target;
+	}
+	/** Any implementation of an ActionListener must include this method so that we can be informed when an action has been performed on our registered controls, allowing us to request focus in the target control.
+	 * @param event An <strong>ActionEvent</strong> with information about the event that fired this method.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    target.requestFocus();
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/Gatherer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/Gatherer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/Gatherer.java	(revision 31635)
@@ -0,0 +1,1718 @@
+/**
+*#########################################################################
+*
+* A component of the Gatherer application, part of the Greenstone digital
+* library suite from the New Zealand Digital Library Project at the
+* University of Waikato, New Zealand.
+*
+* Author: John Thompson, Greenstone Digital Library, University of Waikato
+*
+* Copyright (C) 1999 New Zealand Digital Library Project
+*
+* 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 2 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, write to the Free Software
+* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*########################################################################
+*/
+package org.greenstone.gatherer;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.lang.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.plaf.*;
+import javax.swing.text.*;
+
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.GAuthenticator;
+import org.greenstone.gatherer.FedoraInfo;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.feedback.ActionRecorderDialog;
+import org.greenstone.gatherer.feedback.Base64;
+import org.greenstone.gatherer.file.FileManager;
+import org.greenstone.gatherer.file.FileAssociationManager;
+import org.greenstone.gatherer.file.RecycleBin;
+import org.greenstone.gatherer.greenstone.Classifiers;
+import org.greenstone.gatherer.greenstone.LocalGreenstone;
+import org.greenstone.gatherer.greenstone.LocalLibraryServer;
+import org.greenstone.gatherer.greenstone.Plugins;
+import org.greenstone.gatherer.greenstone3.ServletConfiguration;
+import org.greenstone.gatherer.gui.GUIManager;
+import org.greenstone.gatherer.gui.URLField;
+import org.greenstone.gatherer.gui.WarningDialog;
+import org.greenstone.gatherer.gui.FedoraLogin;
+import org.greenstone.gatherer.metadata.FilenameEncoding;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.GS3ServerThread;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+
+/** Containing the top-level "core" for the Gatherer, this class is the
+* common core for the GLI application and applet. It first parses the
+* command line arguments, preparing to update the configuration as
+* required. Next it loads several important support classes such as the
+* Configuration and Dictionary. Finally it creates the other important
+* managers and sends them on their way.
+* @author John Thompson, Greenstone Digital Library, University of Waikato
+* @version 2.3
+*/
+public class Gatherer
+{
+	/** The name of the GLI. */
+	static final public String PROGRAM_NAME = "Greenstone Librarian Interface";
+	/** The current version of the GLI.
+		* Note: the gs3-release-maker relies on this variable being declared
+		* in a line which matches this java regex:
+		* ^(.*)String\s*PROGRAM_VERSION\s*=\s*"trunk";
+		* If change the declaration and it no longer matches the regex, please
+		* change the regex in the gs3-release-maker code and in this message
+		*/
+
+	static final public String PROGRAM_VERSION = "trunk";
+
+	static private Dimension size = new Dimension(800, 540);
+	static public RemoteGreenstoneServer remoteGreenstoneServer = null;
+
+	/** Has the exit flag been set? */
+	static final public int EXIT_THEN_RESTART= 2;
+	static public boolean exit = false;
+	static public int exit_status = 0;
+
+	static private String gli_directory_path = null;
+	static private String gli_user_directory_path = null;
+
+	static public String client_operating_system = null;
+
+	/** All of the external applications that must exit before we close the Gatherer. */
+	static private Vector apps = new Vector();
+	static private String non_standard_collect_directory_path = null;
+	static public String open_collection_file_path = null;
+	static public String gsdlsite_collecthome = "";
+	/** A public reference to the FileAssociationManager. */
+	static public FileAssociationManager assoc_man;
+	/** A public reference to the CollectionManager. */
+	static public CollectionManager c_man;
+	/** A public reference to the RecycleBin. */
+	static public RecycleBin recycle_bin;
+	/** a reference to the Servlet Configuration is GS3 */
+	static public ServletConfiguration servlet_config;
+	/** A public reference to the FileManager. */
+	static public FileManager f_man;
+	/** A public reference to the GUIManager. */
+	static public GUIManager g_man = null;
+	static private boolean g_man_built = false;
+
+	/** We are using the GLI for GS3 */
+	static public boolean GS3 = false;
+
+	static public boolean isApplet = false;
+	static public boolean isGsdlRemote = false;
+	static public boolean isLocalLibrary = false;
+
+	/* TODO: If we're using local GLI, collections are built locally. If we're using client-GLI
+	* and it contains a gs2build folder in it, then localBuild will also be true (if this is not
+	* turned off in Preferences). If we're remote and this is turned off in Prefs, build remotely. */
+	/*static public boolean buildingLocally = true;*/
+	/** If we're using local GLI, we can always download. If we're using client-GLI, we can only
+	* download if we have a gs2build folder inside it. And if we don't turn off downloadEnabling
+	* in the preferences.
+	*/
+	static public boolean isDownloadEnabled = true;
+
+	// feedback stuff
+	/** is the feedback feature enabled? */
+	static public boolean feedback_enabled = true;
+	/** the action recorder dialog */
+	static public ActionRecorderDialog feedback_dialog = null;
+
+	// Refresh reasons
+	static public final int COLLECTION_OPENED   = 0;
+	static public final int COLLECTION_CLOSED   = 1;
+	static public final int COLLECTION_REBUILT  = 2;
+	static public final int PREFERENCES_CHANGED = 3;
+
+	//////kk added////////  
+	static public String cgiBase="";
+	/////////////////
+
+	/** Magic to allow Enter to fire the default button. */
+	static {
+		KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
+		Keymap map = JTextComponent.getKeymap(JTextComponent.DEFAULT_KEYMAP);
+		map.removeKeyStrokeBinding(enter);
+	}
+
+	static private URL default_gliserver_url=null;
+
+	public Gatherer(String[] args) 
+	{
+		// Display the version to make error reports a lot more useful
+		System.err.println("Version: " + PROGRAM_VERSION + "\n");
+
+		JarTools.initialise(this); 
+
+		GetOpt go = new GetOpt(args);
+
+		// Remember the GSDLOS value
+		client_operating_system = go.client_operating_system;
+
+		// If feedback is enabled, set up the recorder dialog
+		if (go.feedback_enabled) {
+			// Use the default locale for now - this will be changed by the Gatherer run method
+			feedback_enabled = true;
+			feedback_dialog = new ActionRecorderDialog(Locale.getDefault());
+		}
+
+		// Are we using a remote Greenstone?
+		if (go.use_remote_greenstone) {
+			isGsdlRemote = true;
+
+			// We don't have a local Greenstone!
+			go.gsdl3_path=null;
+			go.gsdl3_src_path=null;
+
+			// Don't set go.gsdl_path to null, since gdsl_path may still be set 
+			// if we have a client-gli containing gs2build folder. 
+			// However, keep track of whether we can download.
+			if(go.gsdl_path == null) {
+				isDownloadEnabled = false;
+			}
+
+			// We have to use our own collect directory since we can't use the Greenstone one
+			setCollectDirectoryPath(getGLIUserDirectoryPath() + "collect" + File.separator);
+		}
+		// We have a local Greenstone. OR we have a gs2build folder inside 
+		// the client-GLI folder (with which the Download panel becomes enabled) 
+		if(isDownloadEnabled) {
+			LocalGreenstone.setDirectoryPath(go.gsdl_path);
+		}
+
+		// Users may specify a non-standard collect directory (eg. when running one GLI in a network environment)
+		// Processing of custom non-standard collect directory passed into gli now happens in init()
+		//if (go.collect_directory_path != null) { setCollectDirectoryPath(go.collect_directory_path); }
+
+		// More special code for running with a remote Greenstone
+		if (isGsdlRemote) {
+			if (go.fedora_info.isActive()) {
+				Configuration.TEMPLATE_CONFIG_XML = "xml/" + Configuration.FEDORA_CONFIG_PREFIX + "configRemote.xml";
+			}
+			else {
+				Configuration.TEMPLATE_CONFIG_XML = "xml/configRemote.xml";
+			}
+
+			Configuration.CONFIG_XML = "configRemote.xml"; 
+
+			File collect_directory = new File(Gatherer.getCollectDirectoryPath());
+			if (!collect_directory.exists() && !collect_directory.mkdir()) {
+				System.err.println("Warning: Unable to make directory: " + collect_directory);
+			}
+		}
+		else {	    
+			if (go.fedora_info.isActive()) {
+				Configuration.TEMPLATE_CONFIG_XML = "xml/" + Configuration.FEDORA_CONFIG_PREFIX + Configuration.CONFIG_XML;
+			}
+			// else, the CONFIG_XML uses the default config file, which is for the local GS server
+		}
+
+		init(go.gsdl_path, go.gsdl3_path, go.gsdl3_src_path, 
+		go.fedora_info,
+		go.local_library_path, go.library_url_string, 
+		go.gliserver_url_string, go.debug, go.perl_path, go.no_load, go.filename, go.site_name,
+		go.servlet_path, go.collect_directory_path);
+	}
+
+
+	public void init(String gsdl_path, String gsdl3_path, String gsdl3_src_path, 
+	FedoraInfo fedora_info,
+	String local_library_path,
+	String library_url_string, String gliserver_url_string, boolean debug_enabled,
+	String perl_path, boolean no_load, String open_collection, 
+	String site_name, String servlet_path, String collect_directory_path) 
+	{
+		if (gsdl3_path != null && !gsdl3_path.equals("")) {
+			this.GS3 = true;
+		} else {
+			gsdl3_path = null;
+			gsdl3_src_path = null;
+		}
+
+		// Create the debug stream if required
+		if (debug_enabled) {
+			DebugStream.enableDebugging();
+
+			Calendar now = Calendar.getInstance();
+			String debug_file_path = "debug" + now.get(Calendar.DATE) + "-" + now.get(Calendar.MONTH) + "-" + now.get(Calendar.YEAR) + ".txt";
+
+			// Debug file is created in the user's GLI directory
+			debug_file_path = getGLIUserDirectoryPath() + debug_file_path;
+			DebugStream.println("Debug file path: " + debug_file_path);
+			DebugStream.setDebugFile(debug_file_path);
+			DebugStream.print(System.getProperties());
+		}
+
+		// Delete plugins.dat and classifiers.dat files from previous versions of the GLI (no longer used)
+		File plugins_dat_file = new File(Gatherer.getGLIUserDirectoryPath() + "plugins.dat");
+		if (plugins_dat_file.exists()) {
+			System.err.println("Deleting plugins.dat file...");
+			Utility.delete(plugins_dat_file);
+		}
+		File classifiers_dat_file = new File(Gatherer.getGLIUserDirectoryPath() + "classifiers.dat");
+		if (classifiers_dat_file.exists()) {
+			System.err.println("Deleting classifiers.dat file...");
+			Utility.delete(classifiers_dat_file);
+		}
+
+		try {
+			// Load GLI config file
+			new Configuration(getGLIUserDirectoryPath(), gsdl_path, gsdl3_path, gsdl3_src_path, site_name,
+			fedora_info);
+			
+			// Check we know where Perl is
+			Configuration.perl_path = perl_path;
+			if (isGsdlRemote && !isDownloadEnabled && Utility.isWindows() && Configuration.perl_path != null) {
+				if (Configuration.perl_path.toLowerCase().endsWith("perl.exe")) {
+					Configuration.perl_path = Configuration.perl_path.substring(0, Configuration.perl_path.length() - "perl.exe".length());
+				}
+				if (Configuration.perl_path.endsWith(File.separator)) {
+					Configuration.perl_path = Configuration.perl_path.substring(0, Configuration.perl_path.length() - File.separator.length());
+				}
+			}
+
+			// the feedback dialog has been loaded with a default locale, 
+			// now set the user specified one
+			if (feedback_enabled && feedback_dialog != null) {
+				feedback_dialog.setLocale(Configuration.getLocale("general.locale", true));
+			}
+
+			// Read Dictionary
+			new Dictionary(Configuration.getLocale("general.locale", true), Configuration.getFont("general.font", true));
+
+			// check that we are using Sun Java
+			String java_vendor = System.getProperty("java.vendor");
+			if (!java_vendor.equals("Sun Microsystems Inc.")
+			    && !java_vendor.equals("Oracle Corporation")) {
+				System.err.println(Dictionary.get("General.NotSunJava", java_vendor));
+			}
+
+			// Unless we're using remote building, we need to know where the local Greenstone is
+			if (!isGsdlRemote && gsdl_path == null) {
+				missingGSDL();
+			}
+
+			if (fedora_info.isActive()) {
+
+			    // if we have either a remote GS 2/3 server, or if we are dealing with a 
+			    // local GS2, or else if it's a local GS3 but with fedora installed elsewhere, 
+			    // we ask for fedora login details now
+			    File fedora_webapp = new File(gsdl3_src_path+File.separator+"packages"+File.separator+"tomcat"+File.separator+"webapps"+File.separator+"fedora");
+			    
+			    if(isGsdlRemote || !GS3 || !fedora_webapp.exists()) {
+				popupFedoraInfo();
+			    }
+
+			    // else if we have a local GS3 with fedora installed within it, we'll be starting 
+			    // up fedora further below, together with the local tomcat
+			}
+			
+			// the no_load flag to GLI is processed at the end of handling open_collection_file_path
+			open_collection_file_path = open_collection;
+			if (open_collection_file_path == null) {
+				open_collection_file_path = Configuration.getString(
+				"general.open_collection"+Configuration.gliPropertyNameSuffix(), true);
+			}
+			
+			initCollectDirectoryPath(); 
+			
+			if (no_load || (isGsdlRemote && open_collection_file_path.equals(""))) {
+				open_collection_file_path = null;
+			}
+			
+			// Finally, we're ready to find out the version of the remote Greenstone server 
+			if(isGsdlRemote) {
+				// instantiate the RemoteGreenstoneServer object first
+				remoteGreenstoneServer = new RemoteGreenstoneServer();
+
+				// Set up proxy
+				setProxy();
+				// Now we set up an Authenticator
+				Authenticator.setDefault(new GAuthenticator());
+
+				int greenstoneVersion = 2;
+				requestGLIServerURL();
+				gliserver_url_string = Configuration.gliserver_url.toString();
+				
+				greenstoneVersion = remoteGreenstoneServer.getGreenstoneVersion();
+				// Display the version to make error reports a lot more useful
+				System.err.println("Remote Greenstone server version: " + greenstoneVersion);
+				if(greenstoneVersion == -1) { // remote server not running
+					Gatherer.exit();
+				}
+				if(greenstoneVersion >= 3) {
+					this.GS3 = true;
+					Configuration.prepareForGS3();
+				} 
+				
+				if(fedora_info.isActive()) { 
+					// when GS server is remote, FEDORA_HOME resides on the remote server side,
+					// but we know the library URL from user-provided information
+					library_url_string = fedora_info.getLibraryURL();
+				} else {
+					library_url_string = remoteGreenstoneServer.getLibraryURL(Configuration.gliserver_url.toString());
+				}
+				// write it into the config file
+				Configuration.setString("general.library_url", true, library_url_string);
+			}
+			else { // local greenstone: Start up the local library server, if that's what we want
+				if (!GS3) {		
+					isLocalLibrary = LocalLibraryServer.start(gsdl_path, local_library_path);
+				}
+				else if (!isGsdlRemote) { // local GS3, start the local tomcat
+				    
+					GS3ServerThread thread = new GS3ServerThread(gsdl3_src_path, "restart");
+					thread.start();
+
+					// If fedora installed inside this local GS3, then ask for fedora login details now.
+					if (fedora_info.isActive()) { 
+					    File fedora_webapp = new File(gsdl3_src_path+File.separator+"packages"+File.separator+"tomcat"+File.separator+"webapps"+File.separator+"fedora");
+					    
+					    if(fedora_webapp.exists()) {
+						System.err.println("**** Waiting for the local Greenstone server to start up fedora...");
+						popupFedoraInfo();
+					    }
+					}
+				}
+				// else web library: GS server is local, but doesn't use the webserver included with GS2
+			}
+			
+			// The "-library_url" option overwrites anything in the config files
+			if (library_url_string != null && library_url_string.length() > 0) {
+				try {
+					System.err.println("Setting library_url to " + library_url_string + "...");
+					Configuration.library_url = new URL(library_url_string);
+				}
+				catch (MalformedURLException error) {
+					DebugStream.printStackTrace(error);
+				}
+			}
+
+
+			// Check that we now know the Greenstone library URL, since we need this for previewing collections
+			// It is not necessary if an independent GSI was launched and the user hasn't pressed Enter Library yet
+			DebugStream.println("Configuration.library_url = " + Configuration.library_url);
+			if (Configuration.library_url == null) {
+				if(isLocalLibrary) {
+					if(!LocalLibraryServer.isURLPending()) {
+						missingEXEC();
+					}
+					// else LocalLibraryServer is expecting a URL soon, so don't need to display the dialog
+				} else { // GS2 webLibrary or GS3 or remote Greenstone
+					missingEXEC();
+				}
+			}
+
+			// The "-gliserver_url" option overwrites anything in the config files
+			if (gliserver_url_string != null && gliserver_url_string.length() > 0) {
+				try {
+					System.err.println("Setting gliserver_url to " + gliserver_url_string + "...");
+					Configuration.gliserver_url = new URL(gliserver_url_string);
+				}
+				catch (MalformedURLException error) {
+					DebugStream.printStackTrace(error);
+				}
+			}
+
+			// If we're using a remote Greenstone we need to know where the gliserver script is
+			DebugStream.println("Configuration.gliserver_url = " + Configuration.gliserver_url);
+
+			if (GS3) {
+				// Load Greenstone 3 servlet configuration
+				if (isGsdlRemote){
+					servlet_config = new ServletConfiguration(Configuration.gli_user_directory_path);
+				}else{
+					servlet_config= new ServletConfiguration(gsdl3_path);
+				}
+			}
+
+			if (GS3 && Configuration.servlet_path == null) {
+				Configuration.servlet_path = servlet_config.getServletPath(Configuration.site_name);
+			}
+			
+			// ensure that a directory called 'cache' exists in the GLI user directory  
+			File user_cache_dir = new File(Gatherer.getGLIUserCacheDirectoryPath());
+			System.err.println("User cache dir: " + Gatherer.getGLIUserCacheDirectoryPath());
+			if (!user_cache_dir.exists() && !user_cache_dir.mkdir()) {
+				System.err.println("Warning: Unable to make directory: " + user_cache_dir);
+			}
+
+
+			if (Gatherer.isGsdlRemote) {
+				DebugStream.println("Not checking for perl path/exe");
+			}
+			else {
+				// Perl path is a little different as it is perfectly ok to
+				// start the GLI without providing a perl path
+				boolean found_perl = false;
+				if (Configuration.perl_path != null) {
+					// See if the file pointed to actually exists
+					File perl_file = new File(Configuration.perl_path);
+					found_perl = perl_file.exists();
+					perl_file = null;
+				}
+				if (Configuration.perl_path == null || !found_perl) {
+					// Run test to see if we can run perl as is.
+					PerlTest perl_test = new PerlTest();
+					if (perl_test.found()) {
+						// If so replace the perl path with the system
+						// default (or null for unix).
+						Configuration.perl_path = perl_test.toString();
+						found_perl = true;
+					}
+				}
+				if (!found_perl) {
+					// Time for an error message.
+					missingPERL();
+				}
+			}
+
+
+			// Check for ImageMagick - dependent on perl_path
+			if (Gatherer.isGsdlRemote) {
+				DebugStream.println("Not checking for ImageMagick.");
+			}
+			else if (!(new ImageMagickTest()).found()) {
+				// Time for a warning message
+				missingImageMagick();
+			}
+			
+			// Check for PDFBox
+			if (Gatherer.isGsdlRemote) {
+				DebugStream.println("Not checking for PDFBox.");
+			}
+			else {			    
+				String gs_dir = gsdl_path; // for GS3, pdf-box is in gs2build/ext (not in GS3/ext), for GS2 it's also in GS2/ext
+			    File pdfboxExtensionFolder = new File(gs_dir+File.separator+"ext"+File.separator+"pdf-box");
+			    if (!(pdfboxExtensionFolder.exists() && pdfboxExtensionFolder.isDirectory())) {
+				// The user doesn't have PDFBox, inform them of it
+				String zipExtension = Utility.isWindows() ? "zip" : "tar.gz";
+				missingPDFBox(zipExtension, pdfboxExtensionFolder.getParent());
+			    }
+			}
+
+			// Check that the local can support multiple filename encodings
+			//System.err.println("#### Java identifies current Locale as (file.encoding): "
+			//	+ System.getProperty("file.encoding"));
+			if(System.getProperty("file.encoding").equals("UTF-8")){
+				// If the locale is UTF-8, Java will interpret all filename bytes as UTF-8, 
+				// which is a destructive process as it will convert characters not recognised
+				// by UTF-8 into the invalid character, rather than preserving the bytecodes.
+				// This has the effect that non-UTF8 encoded filenames on a system set to a 
+				// UTF-8 locale are not 'seen' by Java (if they contain non-ASCII characters).
+				multipleFilenameEncodingsNotSupported();		
+				FilenameEncoding.MULTIPLE_FILENAME_ENCODINGS_SUPPORTED = false; 
+				FilenameEncoding.URL_FILE_SEPARATOR = File.separator;
+			} else {
+				FilenameEncoding.MULTIPLE_FILENAME_ENCODINGS_SUPPORTED = true;
+				FilenameEncoding.URL_FILE_SEPARATOR = "/"; // URL file separator is always "/" 
+			}
+			
+			// Set the default font for all Swing components.
+			FontUIResource default_font = Configuration.getFont("general.font", true);
+			Enumeration keys = UIManager.getDefaults().keys();
+			while (keys.hasMoreElements()) {
+				Object key = keys.nextElement();
+				Object value = UIManager.get(key);
+				if (value instanceof FontUIResource) {
+					UIManager.put(key, default_font);
+				}
+			}
+
+			// At this point (which is where this code originally used to be), we can set up the proxy for the
+			// non-remote case. The remote Greenstone server would already have setup its proxy when required.
+			if(!isGsdlRemote) {
+				setProxy();
+				// Now we set up an Authenticator
+				Authenticator.setDefault(new GAuthenticator());
+			}
+			assoc_man = new FileAssociationManager();
+			// Create File Manager
+			f_man = new FileManager();
+			// Create Collection Manager
+			c_man = new CollectionManager();
+			// Create Recycle Bin
+			recycle_bin = new RecycleBin();
+
+			if (GS3) {
+				if (site_name==null) {
+					site_name = Configuration.site_name;
+					servlet_path = null; // need to reset this
+				}
+				if (servlet_path == null) {
+					servlet_path = Configuration.getServletPath();
+				}
+			}
+			
+			
+		} catch (Exception exception) {
+			DebugStream.printStackTrace(exception);
+		}
+
+		
+		// Create GUI Manager (last) or else suffer the death of a thousand NPE's
+		g_man = new GUIManager(size);
+
+		// Get a list of the core Greenstone classifiers and plugins
+		Classifiers.loadClassifiersList(null);
+		Plugins.loadPluginsList(null);
+
+		// Users may specify a non-standard collect directory (eg. when running one GLI in a network environment)
+		// Set to any custom collection directory provided to gli. This happens if gli was run as:
+		// ./gli.sh -collectdir </full/path/to/custom/collect>
+		if(collect_directory_path != null) {
+
+		    // create a version of the collect_dir_path without a file-separator at end
+		    String collectDir = collect_directory_path; 
+		    if(collectDir.endsWith(File.separator)) {
+			collectDir = collectDir.substring(0, collectDir.length()-1); // remove file separator at end
+		    }
+
+		    // update .gli/config.xml to contain the version of the path without file-separator at end
+		    if(collect_directory_path.equals(getDefaultGSCollectDirectoryPath(false))) {
+			Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
+						true, "");
+		    } else {
+			Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
+						true, collectDir);
+		    }
+
+		    // set non_standard_collect_directory_path variable. Ensures file_separator at end
+		    Gatherer.setCollectDirectoryPath(collect_directory_path); 
+
+		    // Use version of path without file-separator at end to set collecthome in gsdlsite(3).cfg
+		    if(collectDir != null) {
+			collectDir = "\""+collectDir+"\"";
+		    }
+		    Utility.updatePropertyConfigFile(getGsdlSiteConfigFile(), "collecthome", collectDir);
+		    // if gsdlsite.cfg does not exist (if using server.exe for instance), the above method will just return
+		}
+
+		// If using a remote Greenstone we need to download the collection configurations now
+		if (Gatherer.isGsdlRemote) {
+			if (remoteGreenstoneServer.downloadCollectionConfigurations().equals("")) {
+				// !! Something went wrong downloading the collection configurations
+				System.err.println("Error: Could not download collection configurations.");
+				if(!Gatherer.isApplet) { // don't close the browser if it is an applet!
+					System.exit(0);
+				}
+			}
+		}
+	}
+
+	
+	/** Returns the correct version of the (local or remote) Greenstone server if init() has already been called. */
+	public static int serverVersionNumber() { 
+		return GS3 ? 3 : 2;
+	} 
+
+	/** Returns "Server: version number" if init() has already been called. */
+	public static String getServerVersionAsString() { 
+		return "Server: v" + serverVersionNumber();
+	} 
+
+	public void openGUI()
+	{
+		// Size and place the frame on the screen
+		Rectangle bounds = Configuration.getBounds("general.bounds", true);
+		if (bounds == null) {
+			// Choose a sensible default value
+			bounds = new Rectangle(0, 0, 640, 480);
+		}
+		
+		// Ensure width and height are reasonable
+		size = bounds.getSize();
+		if (size.width < 640) {
+			size.width = 640;
+		}
+		else if (size.width > Configuration.screen_size.width && Configuration.screen_size.width > 0) {
+			size.width = Configuration.screen_size.width;
+		}
+		if (size.height < 480) {
+			size.height = 480;
+		}
+		else if (size.height > Configuration.screen_size.height && Configuration.screen_size.height > 0) {
+			size.height = Configuration.screen_size.height;
+		}
+
+		if (!g_man_built) {
+
+			g_man.display();
+
+			// Place the window in the desired location on the screen, if this is do-able (not under most linux window managers apparently. In fact you're lucky if they listen to any of your screen size requests).
+			g_man.setLocation(bounds.x, bounds.y);
+			g_man.setVisible(true);
+
+			// After the window has been made visible, check that it is in the correct place
+			// sometimes java places a window not in the correct place, 
+			// but with an offset. If so, we work out what the offset is
+			// and change the desired location to take that into account 
+			Point location = g_man.getLocation();
+			int x_offset = bounds.x - location.x;
+			int y_offset = bounds.y - location.y;
+			// If not, offset the window to move it into the correct location
+			if (x_offset > 0 || y_offset > 0) {
+				///ystem.err.println("changing the location to "+(bounds.x + x_offset)+" "+ (bounds.y + y_offset));
+				g_man.setLocation(bounds.x + x_offset, bounds.y + y_offset);
+			}
+
+			// The 'after-display' triggers several events which don't occur until after the visual components are actually available on screen. Examples of these would be the various html renderings, as they can't happen offscreen.
+			g_man.afterDisplay();
+			g_man_built = true;
+		}
+		else {
+			g_man.setVisible(true);
+		}
+
+		// Get a list of the core Greenstone classifiers and plugins
+		/*Classifiers.loadClassifiersList(null);
+	Plugins.loadPluginsList(null);
+
+	// If using a remote Greenstone we need to download the collection configurations now
+	if (Gatherer.isGsdlRemote) {
+		if (remoteGreenstoneServer.downloadCollectionConfigurations().equals("")) {
+		// !! Something went wrong downloading the collection configurations
+		System.err.println("Error: Could not download collection configurations.");
+		System.exit(0);
+		}
+	}*/
+
+		// If there was a collection left open last time, reopen it	
+		if (open_collection_file_path == null || new File(Gatherer.open_collection_file_path).isDirectory()) {
+			
+			//the menu bar items, file and edit, are disabled from the moment of their creation. if there is no left-over collection from the last session, enable them; otherwise it's disabled until the collection finishes loading. They will be enabled in collectionManager.java
+			setMenuBarEnabled(true);
+		} else {
+
+			// If there was a collection left open last time, reopen it
+			c_man.openCollectionFromLastTime();
+		}
+	}
+
+	public static void setMenuBarEnabled(boolean enabled) {
+		g_man.menu_bar.file.setEnabled(enabled);
+		g_man.menu_bar.edit.setEnabled(enabled);
+	}
+
+	/** Exits the Gatherer after ensuring that things needing saving are saved.
+	* @see java.io.FileOutputStream
+	* @see java.io.PrintStream
+	* @see java.lang.Exception
+	* @see javax.swing.JOptionPane
+	* @see org.greenstone.gatherer.Configuration
+	* @see org.greenstone.gatherer.collection.CollectionManager
+	* @see org.greenstone.gatherer.gui.GUIManager
+	*/
+	static public void exit(int new_exit_status)
+	{
+		DebugStream.println("In Gatherer.exit()...");
+		exit = true;
+		if (new_exit_status != 0) {
+			// default exit_status is already 0 
+			// only remember a new exit status if it is non-trivial
+			exit_status = new_exit_status;
+		}
+
+		// Save the file associations
+		if (assoc_man != null) {
+			assoc_man.save();
+			assoc_man = null;
+		}
+
+		// Get the gui to deallocate
+		if(g_man != null) {
+			g_man.destroy();
+			g_man_built = false;
+		}
+
+		// Flush debug
+		DebugStream.closeDebugStream();
+		
+		// If we started a server, we should try to stop it.
+		if (LocalLibraryServer.isRunning() == true) {		
+			LocalLibraryServer.stop();
+		}
+
+		// If we're using a remote Greenstone server we need to make sure that all jobs have completed first
+		if (isGsdlRemote) {
+			remoteGreenstoneServer.exit();
+		} else if (GS3) { // stop the local tomcat web server when running GS3
+		    // can't call ant stop from its own thread - what if GLI has exited by then?
+		    // issue call to ant stop from the main GLI thread
+		    //GS3ServerThread thread = new GS3ServerThread(Configuration.gsdl_path, "stop");
+		    //thread.start();
+
+		    GS3ServerThread.stopServer();
+		    
+		}
+
+		// Make sure we haven't started up any processes that are still running
+		if (apps.size() == 0) {
+			// If we're running as an applet we don't actually quit (just hide the main GLI window)
+			if (!Gatherer.isApplet) {
+				// This is the end...
+				System.exit(exit_status);
+			}
+		}
+		else {
+			JOptionPane.showMessageDialog(g_man, Dictionary.get("General.Outstanding_Processes"), Dictionary.get("General.Outstanding_Processes_Title"), JOptionPane.ERROR_MESSAGE);
+			g_man.setVisible(false);
+		}
+	}
+
+	static public void exit()
+	{
+		exit(0);
+	}
+
+	/** Returns the path of the current collect directory. */
+	static public String getCollectDirectoryPath()
+	{
+		if (non_standard_collect_directory_path != null) {
+			return non_standard_collect_directory_path;
+		}
+		
+		return getDefaultGSCollectDirectoryPath(true); // file separator appended
+		
+	}
+	
+	// if we need to know whether the local server we are running is server.exe vs apache web server
+	static public boolean isPersistentServer() {
+		return (!isGsdlRemote && LocalLibraryServer.isPersistentServer());
+	}
+
+	/** Returns the path of the Greenstone "collect" directory. */
+	static public String getDefaultGSCollectDirectoryPath(boolean appendSeparator) {
+		String colDir;
+		if (GS3) {
+			colDir = getSitesDirectoryPath() + Configuration.site_name + File.separator + "collect";			
+		}
+		else {
+			colDir = Configuration.gsdl_path + "collect";
+		}
+		
+		if(appendSeparator) {
+			colDir += File.separator;
+		}
+		return colDir;
+	}	
+
+	/** Returns the path of the GLI directory. */
+	static public String getGLIDirectoryPath()
+	{
+		return gli_directory_path;
+	}
+
+
+	/** Returns the path of the GLI "metadata" directory. */
+	static public String getGLIMetadataDirectoryPath()
+	{
+		return getGLIDirectoryPath() + "metadata" + File.separator;
+	}
+
+
+	/** Returns the path of the GLI user directory. */
+	static public String getGLIUserDirectoryPath()
+	{
+		return gli_user_directory_path;
+	}
+
+
+	/** Returns the path of the GLI user "cache" directory. */
+	static public String getGLIUserCacheDirectoryPath()
+	{
+		return getGLIUserDirectoryPath() + "cache" + File.separator;
+	}
+
+
+	/** Returns the path of the GLI user "log" directory. */
+	static public String getGLIUserLogDirectoryPath()
+	{
+		return getGLIUserDirectoryPath() + "log" + File.separator;
+	}
+
+
+	static public String getSitesDirectoryPath()
+	{
+		return Configuration.gsdl3_path + "sites" + File.separator;
+	}
+
+
+	static public void setCollectDirectoryPath(String collect_directory_path)
+	{
+		non_standard_collect_directory_path = collect_directory_path;
+		if (!non_standard_collect_directory_path.endsWith(File.separator)) {
+			non_standard_collect_directory_path = non_standard_collect_directory_path + File.separator;
+		}
+	}
+
+
+	static public void setGLIDirectoryPath(String gli_directory_path_arg)
+	{
+		gli_directory_path = gli_directory_path_arg;
+	}
+
+
+	static public void setGLIUserDirectoryPath(String gli_user_directory_path_arg)
+	{
+		gli_user_directory_path = gli_user_directory_path_arg;
+
+		// Ensure the GLI user directory exists
+		File gli_user_directory = new File(gli_user_directory_path);
+		if (!gli_user_directory.exists() && !gli_user_directory.mkdirs()) {
+			System.err.println("Error: Unable to make directory: " + gli_user_directory);
+		}
+	}
+
+
+	public static void initCollectDirectoryPath() {		
+		String defaultColdir = getDefaultGSCollectDirectoryPath(false); // no file separator at end
+		String coldir = defaultColdir;
+		// If local GS and opening a collection outside the standard GS collect folder,
+		// need to open the non-standard collect folder that the collection resides in
+		if (!isGsdlRemote 
+		    && !open_collection_file_path.startsWith(defaultColdir))
+		{  
+			File collectFolder = null;
+			
+			if(!open_collection_file_path.equals("")) {
+				if(!open_collection_file_path.endsWith("gli.col")) { // then it's a collect folder
+					collectFolder = new File(open_collection_file_path);
+				} else {
+					// the filepath is a gli.col file. To get the collect folder: the 1st level up
+					// is the collection folder, 2 two levels up is the containing collect folder
+					collectFolder = new File(open_collection_file_path).getParentFile().getParentFile();
+				}	
+				
+				// Need to deal with colgroups as well: while there's an etc/collect.cfg
+				// in the current collectFolder, move one level up
+				String cfg_file = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+				if(new File(collectFolder.getAbsolutePath()+File.separator+cfg_file).exists()) { // colgroup
+					collectFolder = collectFolder.getParentFile();		    
+				}
+
+				// Inform the user that their collecthome is non-standard (not inside GS installation)
+				nonStandardCollectHomeMessage(collectFolder.getAbsolutePath(), defaultColdir); // display message
+			}		
+			
+			if(collectFolder == null || !collectFolder.exists()) {
+				// if GLI config file specified no collectDir (open_collection_file_path is "")
+				// OR if dealing with a local server but the collectdir no longer exists, 
+				// use the default greenstone collect directory, and write that to affected files
+				
+				open_collection_file_path = defaultColdir;		// default GS collect dir
+				// Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), true, "");
+			} else { // use the coldir value specified in the flags to GLI or from the last GLI session				
+				coldir = collectFolder.getAbsolutePath();
+			}			
+			// set it as the current folder
+			setCollectDirectoryPath(coldir); // will ensure the required file separator at end
+		}
+		
+		if(!isGsdlRemote) {
+			// LocalLibraryServer would  already have set glisite.cfg to the correct collecthome for server.exe
+			// Here we set collecthome in gsdl(3)site.cfg for the GS2 apache web server and GS3 tomcat server
+			String gsdlsitecfg = getGsdlSiteConfigFile();			
+			// update the gsdlsite config file and store the old value for use when we exit GLI
+			if(coldir.equals(defaultColdir)) {				
+				gsdlsite_collecthome = Utility.updatePropertyConfigFile(
+				gsdlsitecfg, "collecthome", null);
+			} else {
+				gsdlsite_collecthome = Utility.updatePropertyConfigFile(
+				gsdlsitecfg, "collecthome", "\""+coldir+"\""); // no file separator
+				// if gsdlsite.cfg does not exist (if using server.exe for instance), the above method will just return
+			}
+		}		
+	}
+
+	/** depending on the version of GS being run, return the path to the current GS' installation's gsdl(3)site.cfg */
+	public static String getGsdlSiteConfigFile() {
+		if(Gatherer.GS3) { // web/WEB-INF/cgi/gsdl3site.cfg
+			return Configuration.gsdl3_path + File.separator + "WEB-INF" 
+			+ File.separator + "cgi" + File.separator + "gsdl3site.cfg";
+		} else { // cgi-bin/gsdlsite.cfg
+		    String gsdlarch = System.getenv("GSDLARCH");
+		    if(gsdlarch == null) {
+			gsdlarch = "";
+		    }
+		    return Configuration.gsdl_path /* + File.separator */
+			+ "cgi-bin" + File.separator + client_operating_system+gsdlarch + File.separator + "gsdlsite.cfg";
+		}
+	}
+
+	public static void collectDirectoryHasChanged(
+	String oldCollectPath, String newCollectPath, final Component container) 
+	{		
+		if(oldCollectPath.equals(newCollectPath)) {
+			return; // nothing to be done
+		}
+		
+		// Will use a busy cursor if the process of changing the collect directory takes more 
+		// than half a second/500ms. See http://www.catalysoft.com/articles/busyCursor.html
+		Cursor originalCursor = container.getCursor();
+		java.util.TimerTask timerTask = new java.util.TimerTask() {
+			public void run() {
+				// set the cursor on the container:
+				container.setCursor(new Cursor(Cursor.WAIT_CURSOR));
+			}
+		};
+		java.util.Timer timer = new java.util.Timer(); 
+		
+		try {
+			timer.schedule(timerTask, 500);
+			
+			// first save any open collection in the old location, then close it
+			if(Gatherer.c_man.getCollection() != null) {
+				Gatherer.g_man.saveThenCloseCurrentCollection(); // close the current collection first
+			}
+			
+			// change to new collect path
+			if(newCollectPath.equals(getDefaultGSCollectDirectoryPath(true))) {
+				Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
+				true, "");
+			} else {
+				Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
+				true, newCollectPath);
+			}
+			Gatherer.setCollectDirectoryPath(newCollectPath);
+			
+
+			// refresh the Documents in Greenstone Collections
+			//WorkspaceTreeModel.refreshGreenstoneCollectionsNode();
+			Gatherer.g_man.refreshWorkspaceTreeGreenstoneCollections();
+			
+			// The web server needs to be told where a new (non-standard) collecthome home is.
+			// The web server reads collecthome from cgi-bin/<OS>/gsdlsite.cfg, where the property
+			// collecthome can be specified if a non-standard collecthome is to be used. If no 
+			// such property is specified in the file, then it assumes the standard GS collecthome.
+			// This method does nothing for a remote Greenstone.	
+			if(Gatherer.isGsdlRemote) {
+				return;
+			}	
+			
+			// non-destructive update of gsdl(3)site.cfg (comments preserved)
+			String collectDir = Gatherer.getCollectDirectoryPath();
+			//collectDir = "\"" + collectDir.substring(0, collectDir.length()-1) + "\""; // remove file separator at end	
+			collectDir = collectDir.substring(0, collectDir.length()-1); // remove file separator at end
+			if(collectDir != null) {
+				collectDir = "\""+collectDir+"\"";
+			}
+			Utility.updatePropertyConfigFile(getGsdlSiteConfigFile(), "collecthome", collectDir);
+			// if gsdlsite.cfg does not exist (if using server.exe for instance), the above method will just return
+			
+			if(!Gatherer.GS3 && Gatherer.isLocalLibrary) {
+				// for Images in the collection to work, the apache web server 
+				// configuration's COLLECTHOME should be updated on collectdir change. 
+				// Does nothing for server.exe at the moment
+
+				LocalLibraryServer.reconfigure();
+			}
+		} finally { // Note try-finally section without catch:
+			// "Java's finally clause is guaranteed to be executed even when
+			// an exception is thrown and not caught in the current scope."
+			// See http://www.catalysoft.com/articles/busyCursor.html
+			// the following code fragment is guaranteed to restore the original
+			// cursor now the custom actionPerformed() processing is complete, regardless
+			// of whether the processing method terminates normally or throws an exception
+			// and regardless of where in the call stack the exception is caught.			
+			
+			timer.cancel();
+			container.setCursor(originalCursor);
+		}
+	}
+
+
+	static public void refresh(int refresh_reason)
+	{
+		if (g_man != null) {
+
+			g_man.refresh(refresh_reason, c_man.ready());
+		}
+
+		// Now is a good time to force a garbage collect
+		System.gc();
+	}
+
+
+	// used to send reload coll messages to the tomcat server
+	static public void configGS3Server(String site, String command) {
+
+	    // Do not do configGS3Server for a GS3 solr collection. Not at present at least, when we're stopping
+	    // and starting the GS3 server from GLI for solr cols, since this function clears the solr.xml file.
+	    /*if(Gatherer.c_man.isSolrCollection()) {
+		return;
+		}*/
+
+		if (Configuration.library_url == null){
+			System.out.println("Error: you have not provided the Greenstone Library address.");  
+			return;
+
+		} 
+		
+		try {
+			// need to add the servlet name to the exec address
+			String raw_url = Configuration.library_url.toString() + Configuration.getServletPath() + command;
+			URL url = new URL(raw_url);
+			DebugStream.println("Action: " + raw_url);
+			HttpURLConnection library_connection = (HttpURLConnection) url.openConnection();
+			int response_code = library_connection.getResponseCode();
+			if(HttpURLConnection.HTTP_OK <= response_code && response_code < HttpURLConnection.HTTP_MULT_CHOICE) {
+				DebugStream.println("200 - Complete.");
+			}
+			else {
+				DebugStream.println("404 - Failed.");
+			}
+			url = null;
+		}
+		catch(java.net.ConnectException connectException) {
+			JOptionPane.showMessageDialog(g_man, Dictionary.get("Preferences.Connection.Library_Path_Connection_Failure", Configuration.library_url.toString()), Dictionary.get("General.Warning"), JOptionPane.WARNING_MESSAGE);
+			DebugStream.println(connectException.getMessage());
+		}
+		catch (Exception exception) {            
+			DebugStream.printStackTrace(exception);
+		}
+	}
+
+
+	/** Used to 'spawn' a new child application when a file is double clicked.
+	* @param file The file to open 
+	* @see org.greenstone.gatherer.Gatherer.ExternalApplication
+	*/
+	static public void spawnApplication(File file) {
+		String [] commands = assoc_man.getCommand(file);
+		if(commands != null) {
+			ExternalApplication app = new ExternalApplication(commands);
+			apps.add(app);
+			app.start();
+		}
+		else {
+			///ystem.err.println("No open command available.");
+		}
+	}
+
+
+	static public void spawnApplication(String command)
+	{
+		ExternalApplication app = new ExternalApplication(command);
+		apps.add(app);
+		app.start();
+	}
+
+	static public void spawnApplication(String command, String ID)
+	{
+		ExternalApplication app = new ExternalApplication(command, ID);
+		apps.add(app);
+		app.start();
+	}
+
+	static public void spawnApplication(String[] commands, String ID)
+	{
+		ExternalApplication app = new ExternalApplication(commands, ID);
+		apps.add(app);
+		app.start();
+	}
+
+	static public void terminateApplication(String ID) {
+		for(int i = 0; i < apps.size(); i++) {
+			ExternalApplication app = (ExternalApplication)apps.get(i);
+			if(app.getID() != null && app.getID().equals(ID)) {
+				app.stopExternalApplication();
+				apps.remove(app);
+			}
+		}
+	}
+
+
+	/** Used to 'spawn' a new  browser application or reset an existing one when the preview button is clicked 
+	* @param url The url to open the browser at
+	* @see org.greenstone.gatherer.Gatherer.BrowserApplication
+	*/    
+	static public void spawnBrowser(String url) {
+		String command = assoc_man.getBrowserCommand(url);
+		if (command != null) {
+			BrowserApplication app = new BrowserApplication(command, url);
+			apps.add(app);
+			app.start();
+		}
+		else {
+			///ystem.err.println("No browser command available."); 
+		}
+	}
+
+
+	/** Prints a warning message about a missing library path, which means the final collection cannot be previewed in the Gatherer.
+	*/
+	static public void missingEXEC() {
+		WarningDialog dialog;
+		String configPropertyName = "general.library_url"+Configuration.gliPropertyNameSuffix();
+
+		if (GS3) {
+			// Warning dialog with no cancel button and no "turn off warning" checkbox 
+			dialog = new WarningDialog("warning.MissingEXEC", Dictionary.get("MissingEXEC_GS3.Title"), Dictionary.get("MissingEXEC_GS3.Message"), configPropertyName, false, false);
+		} else { // local case
+			dialog = new WarningDialog("warning.MissingEXEC", Dictionary.get("MissingEXEC.Title"), Dictionary.get("MissingEXEC.Message"), configPropertyName, false);
+		}
+
+		JTextField field = new URLField.Text(Configuration.getColor("coloring.editable_foreground", false), Configuration.getColor("coloring.editable_background", false));
+
+		// Set the default library URL to the tomcat server and port number
+		// specified in the build.properties located in the gsdl3_src_path 
+		if (GS3) {
+			String host = "localhost";
+			String port = "8383";
+			
+			File buildPropsFile = new File(Configuration.gsdl3_src_path + File.separator + "build.properties");
+			if(buildPropsFile.exists()) {
+				Properties props = new Properties();
+				try{
+					props.load(new FileInputStream(buildPropsFile));
+					host = props.getProperty("tomcat.server", host); 
+					port = props.getProperty("tomcat.port", port);
+				}catch(Exception e){
+					DebugStream.println("Could not load build.properties file");
+					System.err.println("Could not load build.properties file");
+				}
+				props = null;
+			}
+			String defaultURL = "http://"+host+":"+port+"/"+"greenstone3";
+			field.setText(defaultURL);
+			field.selectAll();
+		}
+		dialog.setValueField(field);
+		dialog.display();
+		dialog.dispose();
+		dialog = null;
+
+		String library_url_string = Configuration.getString(configPropertyName, true);
+		if (!library_url_string.equals("")) {
+			try {
+				// WarningDialog does not allow invalid URLs, so the following is ignored:
+				// make sure the URL the user provided contains the http:// prefix
+				// and then save the corrected URL
+				if(!library_url_string.startsWith("http://") 
+						&& !library_url_string.startsWith("https://")) {
+					library_url_string = "http://"+library_url_string;
+					Configuration.setString(configPropertyName, true, configPropertyName);
+				}
+				Configuration.library_url = new URL(library_url_string);
+			}
+			catch (MalformedURLException exception) {
+				DebugStream.printStackTrace(exception);
+			}
+		}
+	}
+
+
+
+	/** Prints a warning message about a missing library path, which means the final collection cannot be previewed in the Gatherer.
+	*/
+	static private void popupFedoraInfo() {
+
+		FedoraLogin dialog = new FedoraLogin("Fedora Login", false);
+
+		if (Configuration.library_url == null) {
+			
+			String library_url_string = dialog.getLibraryURL();
+			if (!library_url_string.equals("")) {
+				try {
+					Configuration.library_url = new URL(library_url_string);
+				}
+				catch (MalformedURLException exception) {
+					DebugStream.printStackTrace(exception);
+				}
+			}
+		}
+		
+		boolean showLogin = true;
+		do {
+			if(!dialog.loginRequested()) { // user pressed cancel to exit the FedoraLogin dialog
+				System.exit(0);
+			} else {
+				showLogin = dialog.loginRequested();
+				String hostname = dialog.getHostname();
+				String port     = dialog.getPort();
+				String username = dialog.getUsername();
+				String password = dialog.getPassword();
+				String protocol = dialog.getProtocol();
+				
+				Configuration.fedora_info.setHostname(hostname);
+				Configuration.fedora_info.setPort(port);
+				Configuration.fedora_info.setUsername(username);
+				Configuration.fedora_info.setPassword(password);
+				Configuration.fedora_info.setProtocol(protocol);
+				
+				String ping_url_str = protocol + "://" + hostname + ":" + port + "/fedora";
+				String login_str = username + ":" + password;
+				
+				String login_encoding = Base64.encodeBytes(login_str.getBytes());
+				
+				try {
+					URL ping_url = new URL(ping_url_str);
+					URLConnection uc = ping_url.openConnection();
+					uc.setRequestProperty  ("Authorization", "Basic " + login_encoding);
+					// Attempt to access some content ...
+					InputStream content = (InputStream)uc.getInputStream();
+					
+					// if no exception occurred in the above, we would have come here:
+					showLogin = false;
+					dialog.dispose();
+				}
+				catch (Exception exception) {
+					// TODO: move into dictionary
+					String[] errorMessage = {"Failed to connect to the Fedora server.", "It might not be running, or",
+						"incorrect username and/or password."};
+					dialog.setErrorMessage(errorMessage);
+					//DebugStream.printStackTrace(exception);
+					// exception occurred, show the dialog again (do this after printing to
+					// debugStream, else the above does not get done for some reason).
+					dialog.setVisible(true);
+				}
+			} 
+		} while(showLogin);
+
+		dialog = null; // no more need of the dialog
+		
+		// Now we are connected.
+	}
+
+
+
+	static private void requestGLIServerURL()
+	{
+		WarningDialog dialog;
+		String[] defaultURLs = {
+			"http://localhost:8080/greenstone3/cgi-bin/gliserver.pl", 
+			"http://localhost:8080/gsdl/cgi-bin/gliserver.pl"
+		};
+
+		// Warning dialog with no cancel button and no "turn off warning" checkbox 
+		// (since user-input of the gliserver script is mandatory)
+		dialog = new WarningDialog("warning.MissingGLIServer", Dictionary.get("MissingGLIServer.Title"), Dictionary.get("MissingGLIServer.Message"), "general.gliserver_url", false, false); 
+
+		dialog.setValueField(new URLField.DropDown(Configuration.getColor("coloring.editable_foreground", false), 
+		Configuration.getColor("coloring.editable_background", false), 
+		defaultURLs, "general.gliserver_url", 
+		"general.open_collection"+Configuration.gliPropertyNameSuffix(), 
+		"gliserver.pl")); 
+
+		if (Gatherer.default_gliserver_url!=null){
+			dialog.setValueField(Gatherer.default_gliserver_url.toString());
+		}
+
+		// A WarningDialog cannot always be made to respond (let alone to exit the program) on close. We
+		// handle the response of this particular WarningDialog here: a URL for gliserver.pl is a crucial
+		// piece of user-provided data. Therefore, if no URL was entered for gliserver.pl, it'll exit safely.
+		dialog.addWindowListener(new WindowAdapter() {
+			public void windowClosing(WindowEvent e) {
+				Gatherer.exit();
+			}
+		});
+		
+		dialog.display();
+		dialog.dispose();
+		dialog = null;
+
+		
+		String gliserver_url_string = Configuration.getString("general.gliserver_url", true);
+		if (!gliserver_url_string.equals("")) {
+			try {
+				Configuration.gliserver_url = new URL(gliserver_url_string);
+				Configuration.setString("general.gliserver_url", true, gliserver_url_string);
+			}
+			catch (MalformedURLException exception) {
+				DebugStream.printStackTrace(exception);
+			}
+		}
+	}
+
+
+	/** Prints a warning message about a missing GSDL path, which although not fatal pretty much ensures nothing will work properly in the GLI.
+	*/
+	static private void missingGSDL() {
+		WarningDialog dialog = new WarningDialog("warning.MissingGSDL", Dictionary.get("MissingGSDL.Title"), Dictionary.get("MissingGSDL.Message"), null, false);
+		dialog.display();
+		dialog.dispose();
+		dialog = null;
+	}
+
+	/** Prints a warning message about missing a valid ImageMagick path, which although not fatal means building image collections won't work */
+	static private void missingImageMagick() {
+		WarningDialog dialog = new WarningDialog("warning.MissingImageMagick", Dictionary.get("MissingImageMagick.Title"), Dictionary.get("MissingImageMagick.Message"), null, false);
+		dialog.display();
+		dialog.dispose();
+		dialog = null;
+	}
+
+    	/** Prints a message informing the user where they can get PDFBox from to process PDF files of v1.5 and greater */
+    static private void missingPDFBox(String zipExtension, String extFolder) {
+	// point to the correct version of the PDFBox extension for this Greenstone release
+	String releaseTag = "";
+	if(!PROGRAM_VERSION.equals("trunk")) { // assume it's a release version
+	    releaseTag = "main/tags/"+PROGRAM_VERSION+"/";
+	}
+
+	WarningDialog dialog = new WarningDialog("warning.MissingPDFBox", Dictionary.get("MissingPDFBox.Title"), Dictionary.get("MissingPDFBox.Message", new String[]{releaseTag, zipExtension, extFolder}), null, false);
+		dialog.display();
+		dialog.dispose();
+		dialog = null;
+	}
+
+	/** Prints a warning message about missing a valid PERL path, which although not fatal pretty much ensures no collection creation/building will work properly in the GLI. */
+	static private void missingPERL() {
+		WarningDialog dialog = new WarningDialog("warning.MissingPERL", Dictionary.get("MissingPERL.Title"), Dictionary.get("MissingPERL.Message"), null, false);
+		dialog.display();
+		dialog.dispose();
+		dialog = null;
+	}
+
+    /** Prints a message informing the user that their collecthome is non-standard (not inside GS installation) */
+    static private void nonStandardCollectHomeMessage(String open_collection_file_path, String defaultColDir) {
+	WarningDialog dialog = new WarningDialog("warning.NonStandardCollectHome", Dictionary.get("NonStandardCollectHome.Title"), Dictionary.get("NonStandardCollectHome.Message", new String[]{open_collection_file_path, defaultColDir}), null, false);
+		dialog.display();
+		dialog.dispose();
+		dialog = null;
+	}
+
+	/** Prints a warning message about the OS not supporting multiple filename encodings.  */
+	static private void multipleFilenameEncodingsNotSupported() {	
+		WarningDialog dialog = new WarningDialog("warning.NoEncodingSupport", 
+		Dictionary.get("NoEncodingSupport.Title"), 
+		Dictionary.get("NoEncodingSupport.Message"), null, false);
+		dialog.display();
+		dialog.dispose();
+		dialog = null;
+	}
+
+	/** Sets up the proxy connection by setting JVM Environment flags and creating a new Authenticator.
+	* @see java.lang.Exception
+	* @see java.lang.System
+	* @see java.net.Authenticator
+	* @see org.greenstone.gatherer.Configuration
+	* @see org.greenstone.gatherer.GAuthenticator
+	*/
+	static public void setProxy() {
+		try {// Can throw several exceptions
+			if(Configuration.get("general.use_proxy", true)) {
+				System.setProperty("http.proxyType", "4");
+				System.setProperty("http.proxyHost", Configuration.getString("general.proxy_host", true));
+				System.setProperty("http.proxyPort", Configuration.getString("general.proxy_port", true));
+				System.setProperty("http.proxySet", "true");
+			} else {
+				System.setProperty("http.proxyHost", "");
+				System.setProperty("http.proxyPort", "");
+				System.setProperty("http.proxySet", "false");
+			}
+		} catch (Exception error) {
+			DebugStream.println("Error in Gatherer.initProxy(): " + error);
+			DebugStream.printStackTrace(error);
+		}
+	}
+
+	
+	/** This private class contains an instance of an external application running within a JVM shell. It is important that this process sits in its own thread, but its more important that when we exit the Gatherer we don't actually System.exit(0) the Gatherer object until the user has volunteerily ended all of these child processes. Otherwise when we quit the Gatherer any changes the users may have made in external programs will be lost and the child processes are automatically deallocated. */
+	static private class ExternalApplication
+	extends Thread {
+		private Process process = null;
+		/** The initial command string given to this sub-process. */
+		private String command = null;
+		private String[] commands = null;
+
+		private String ID = null;
+
+		/** Constructor.
+	* @param command The initial command <strong>String</strong>.
+	*/
+		public ExternalApplication(String command) {
+			this.command = command;
+		}
+
+		public ExternalApplication(String[] commands) {
+			this.commands = commands;
+		}
+
+		public ExternalApplication(String command, String ID) {
+			this.command = command;
+			this.ID = ID;
+		}
+
+		public ExternalApplication(String[] commands, String ID) {
+			this.commands = commands;
+			this.ID = ID;
+		}
+		
+		public String getID() { 
+			return ID; 
+		}
+
+		/** We start the child process inside a new thread so it doesn't block the rest of Gatherer.
+	* @see java.lang.Exception
+	* @see java.lang.Process
+	* @see java.lang.Runtime
+	* @see java.lang.System
+	* @see java.util.Vector
+	*/
+		public void run() {
+			// Call an external process using the args.
+			try {
+				if(commands != null) {
+					StringBuffer whole_command = new StringBuffer();
+					for(int i = 0; i < commands.length; i++) {						
+						// get rid of any quotes around parameters in file associations
+						if(commands[i].startsWith("\"") || commands[i].startsWith("\'")) {
+						    commands[i] = commands[i].substring(1);
+						}
+						if(commands[i].endsWith("\"") || commands[i].endsWith("\'")) {
+						    commands[i] = commands[i].substring(0, commands[i].length()-1);
+						}
+
+						if (i>0) {
+						    whole_command.append(" ");
+						}
+						whole_command.append(commands[i]);		    
+					}
+					DebugStream.println("Running " + whole_command.toString());
+					Runtime rt = Runtime.getRuntime();
+					process = rt.exec(commands);
+					process.waitFor();
+				}
+				else {
+					DebugStream.println("Running " + command);
+					Runtime rt = Runtime.getRuntime();
+					process = rt.exec(command);
+					process.waitFor();
+				}
+			}
+			catch (Exception exception) {
+				DebugStream.printStackTrace(exception);
+			}
+			// Remove ourself from Gatherer list of threads.
+			apps.remove(this);
+			// Call exit if we were the last outstanding child process thread.
+			if (apps.size() == 0 && exit == true) {
+				// In my opinion (DB) there is no need to exit here,
+				// the 'run' method ending naturally brings this
+				// thread to an end.  In fact it is potentially
+				// dangerous to exit here, as the main thread in the
+				// Gatherer class may be stopped prematurely.  As it so
+				// happens the point at which the ExternalApplication thread
+				// is asked to stop (Back in the main Gatherer thread) is after
+				// various configuration files have been saved.
+				// 
+				// A similar argument holds for BrowserApplication thread below.
+				System.exit(exit_status);
+			}
+		}
+		public void stopExternalApplication() {
+			if(process != null) {
+				process.destroy();
+			}
+		}
+	}
+	/** This private class contains an instance of an external application running within a JVM shell. It is important that this process sits in its own thread, but its more important that when we exit the Gatherer we don't actually System.exit(0) the Gatherer object until the user has volunteerily ended all of these child processes. Otherwise when we quit the Gatherer any changes the users may have made in external programs will be lost and the child processes are automatically deallocated. */
+	static private class BrowserApplication
+	extends Thread {
+		private Process process = null;
+		/** The initial command string given to this sub-process. */
+		private String command = null;
+		private String url = null;
+		private String[] commands = null;
+
+		public BrowserApplication(String command, String url) {
+			StringTokenizer st = new StringTokenizer(command);
+			int num_tokens = st.countTokens();
+			this.commands = new String [num_tokens];
+			int i=0;
+			while (st.hasMoreTokens()) {
+				commands[i] = st.nextToken();
+				i++;
+			}
+			//this.commands = commands;
+			this.url = url;
+		}
+		/** We start the child process inside a new thread so it doesn't block the rest of Gatherer.
+	* @see java.lang.Exception
+	* @see java.lang.Process
+	* @see java.lang.Runtime
+	* @see java.lang.System
+	* @see java.util.Vector
+	*/
+		public void run() {
+			// Call an external process using the args.
+			if(commands == null) {
+				apps.remove(this);
+				return;
+			}
+			try {
+				String prog_name = commands[0];
+				String lower_name = prog_name.toLowerCase();
+				if (lower_name.indexOf("mozilla") != -1 || lower_name.indexOf("netscape") != -1) { 
+					DebugStream.println("found mozilla or netscape, trying remote it");
+					// mozilla and netscape, try using a remote command to get things in the same window
+					String [] new_commands = new String[] {prog_name, "-raise", "-remote", "openURL("+url+",new-tab)"};
+					printArray(new_commands);
+
+					Runtime rt = Runtime.getRuntime();
+					process = rt.exec(new_commands);
+					int exitCode = process.waitFor();
+					if (exitCode != 0) { // if Netscape or mozilla was not open
+						DebugStream.println("couldn't do remote, trying original command");
+						printArray(commands);
+						process = rt.exec(commands); // try the original command
+					}
+				} else {
+					// just run what we have been given
+					StringBuffer whole_command = new StringBuffer();
+					for(int i = 0; i < commands.length; i++) {
+						whole_command.append(commands[i]);
+						whole_command.append(" ");
+					}
+					DebugStream.println("Running " + whole_command.toString());
+					Runtime rt = Runtime.getRuntime();
+					process = rt.exec(commands);
+					process.waitFor();
+				}
+			}
+			
+			catch (Exception exception) {
+				DebugStream.printStackTrace(exception);
+			}
+			// Remove ourself from Gatherer list of threads.
+			apps.remove(this);
+			// Call exit if we were the last outstanding child process thread.
+			if (apps.size() == 0 && exit == true) {
+				System.exit(exit_status);
+			}
+		}
+		public void printArray(String [] array) {
+			for(int i = 0; i < array.length; i++) {
+				DebugStream.print(array[i]+" ");
+				System.err.println(array[i]+" ");
+			}
+		}
+		public void stopBrowserApplication() {
+			if(process != null) {
+				process.destroy();
+			}
+		}
+	}
+
+
+	private class ImageMagickTest
+	{
+		public boolean found()
+		{
+		    // at this stage, GLI has already sourced setup.bash, and the necessary
+		    // env variables will be available to the perl process we're about to launch
+		    boolean found = false;
+
+			try {
+			    // run the command `/path/to/perl -S gs-magick.pl identify -version`
+			    ArrayList cmd_list = new ArrayList();
+			    if (!Gatherer.isGsdlRemote) {
+				if(Configuration.perl_path != null) {
+				    cmd_list.add(Configuration.perl_path);
+				} else {
+				    System.err.println("***** ImageMagickTest Warning: Perl_path not set, calling 'perl' instead.");
+				    cmd_list.add("perl");
+				}
+				cmd_list.add("-S");
+			    }
+			    cmd_list.add("gs-magick.pl");
+			    if(Utility.isWindows()) {
+				cmd_list.add("identify.exe");
+			    } else {
+				cmd_list.add("identify");
+			    }
+			    cmd_list.add("-version");
+
+			    String[] command_parts = (String[]) cmd_list.toArray(new String[0]);
+
+			    String cmd_str = "";
+			    for(int i = 0; i < command_parts.length; i++) {
+			    	cmd_str += command_parts[i] + " ";
+			    }
+			    DebugStream.println("***** Running ImageMagickTest command: " + cmd_str);
+
+				Process image_magick_process = Runtime.getRuntime().exec(command_parts);
+				image_magick_process.waitFor();
+
+				//new way of detection of ImageMagick
+				InputStreamReader isr = new InputStreamReader(image_magick_process.getInputStream());
+
+				BufferedReader br = new BufferedReader(isr);
+				// Capture the standard output stream and seach for two particular occurrences: Version and ImageMagick.
+
+				String line = br.readLine();
+				if (line != null) {
+				    String lc_line = line.toLowerCase();
+				    if (lc_line.indexOf("version") != -1 || lc_line.indexOf("imagemagick") != -1) {
+					//System.err.println("*** ImageMagickTest Line: " + line);
+					found = true;
+				    } // else found var remains false
+				}
+
+				// Maybe put the close in a finally (but note that it can throw and IOex too)? See
+				// http://download.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
+				br.close();
+				return found;
+				//return (image_magick_process.exitValue() == 0);
+			}
+			catch (Exception exception) {	
+			    exception.printStackTrace();
+			    return found;
+			}
+		}
+	}
+
+
+	private class PerlTest
+	{
+		private String[] command = new String[2];
+
+		public PerlTest()
+		{
+			command[0] = (Utility.isWindows() ? Utility.PERL_EXECUTABLE_WINDOWS : Utility.PERL_EXECUTABLE_UNIX);
+			command[1] = "-version";
+		}
+
+		public boolean found()
+		{
+			try {
+				Process perl_process = Runtime.getRuntime().exec(command);
+				perl_process.waitFor();
+				return (perl_process.exitValue() == 0);
+			}
+			catch (Exception exception) {
+				return false;
+			}
+		}
+
+		public String toString() {
+			return command[0];
+		}
+	}
+	
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GathererApplet.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GathererApplet.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GathererApplet.java	(revision 31635)
@@ -0,0 +1,230 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer;
+
+import java.applet.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.net.*;
+import javax.swing.JApplet;
+
+import java.io.*;
+import javax.swing.*;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.UnzipTools;
+import org.greenstone.gatherer.util.Utility;
+
+
+public class GathererApplet extends JApplet implements ActionListener
+{
+    private Gatherer gatherer = null;
+
+
+    protected String fullLibraryURL(String address) 
+    {
+	String full_address = "";
+
+        // make sure the URL has protocol, host, and file
+        if (address.startsWith("http:")) {
+            // the address has all the necessary components
+	    full_address = address;
+        } 
+	else if (address.startsWith("/")) {
+            // there is not protocol and host
+            URL document = getDocumentBase();
+	    int port_no = document.getPort();
+
+            String port = (port_no>0) ? ":" + port_no : "";
+            full_address = "http://" + document.getHost() + port  + address;
+	}
+
+	return full_address;
+    }
+
+
+    /* This is the entry point for the GLI applet */
+    public void init()
+    {
+	JarTools.initialise(this); 
+
+	// Check if the user has agreed to the requested security settings
+	try {
+	    // Work-around to reduce number of 
+	    // 'Could not lock user prefs' error messages.
+	    // Thanks to Walter Schatz from the java forums.
+	    System.setProperty("java.util.prefs.syncInterval","2000000");
+	}
+	catch (Exception exception) {
+	    getContentPane().add(new JLabel("Greenstone Librarian Interface Applet deactivated", JLabel.CENTER));
+	    return;
+	}
+
+	// Ensure platform specific LAF
+	try {
+	    //UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+	    UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
+	}
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	}
+
+	// Determine the GLI user directory path
+	String gli_user_directory_path = System.getProperty("user.home") + File.separator;
+        if (Utility.isWindows()) {
+	    String windows_home_parameter = getParameter("windowsHome");
+	    if (windows_home_parameter != null && !windows_home_parameter.equals("")) {
+		gli_user_directory_path = windows_home_parameter + File.separator + "GLI" + File.separator;
+	    }
+	    else {
+		gli_user_directory_path += "Application Data" + File.separator + "Greenstone" + File.separator + "GLI" + File.separator;
+	    }
+	}
+        else {
+            gli_user_directory_path += ".gli" + File.separator;
+        }
+	Gatherer.setGLIUserDirectoryPath(gli_user_directory_path);
+
+	// If we're running as an applet we don't have a local GLI, so use the user directory path as the GLI path
+	Gatherer.setGLIDirectoryPath(gli_user_directory_path);
+
+	// Set some global variables so we know how we're running
+	Gatherer.isApplet = true;
+	String library_url_string = fullLibraryURL(getParameter("gwcgi"));
+
+	String gs3 = getParameter("gsdl3");
+	boolean isGS3 = (gs3 == null || !gs3.equals("true")) ? false : true;
+
+	String gliserver_url_string;
+	if(!isGS3) {
+	    gliserver_url_string = library_url_string.substring(0, library_url_string.lastIndexOf('/') + 1) + "gliserver.pl";
+	} else {
+	    gliserver_url_string = library_url_string + "/cgi-bin/gliserver.pl";
+	}
+
+        // String debug_param = getParameter("debug");
+        // if ((debug_param != null) && (debug_param.matches("true"))) {
+        //     go.debug = true;
+        // }
+
+	String[] args;
+	if(!isGS3) {
+	    String[] gs2_args = { 
+		"-use_remote_greenstone",
+		"-gliserver_url", gliserver_url_string,
+		"-library_url", library_url_string,
+		// "-debug",
+	    };
+	    args = gs2_args;
+	} else { // >= GS3
+	    String[] gs3_args = {
+		"-use_remote_greenstone",
+		"-gliserver_url", gliserver_url_string,
+		"-library_url", library_url_string,
+		"-gsdl3",
+		//"-debug",
+	    };
+	    args = gs3_args;
+	}
+	
+	File collect_directory = new File(Gatherer.getCollectDirectoryPath());
+	if (!collect_directory.exists()) {
+	    if (JarTools.isInJar("collect.zip")) {
+		// Dig out collect.zip from JAR file and unzip it (thereby
+		// creating collect directory and descendants.
+		//   This is done to seed gsdl home folder with config files
+		//   for documented example collections so "base this on
+		//   existing collection" works
+		unzipFromJar("collect.zip", Gatherer.getGLIUserDirectoryPath());
+	    }
+	    else {
+		// Prepare an empty collect dir for use
+		if (!collect_directory.mkdir()) {
+		    System.err.println("Warning: Unable to make directory: " + collect_directory);
+		}
+	    }
+	}
+
+	File metadata_directory = new File(Gatherer.getGLIMetadataDirectoryPath());
+	if (!metadata_directory.exists()) {
+	    // dig out metadata.zip from JAR file and unzip it
+	    unzipFromJar("metadata.zip", Gatherer.getGLIDirectoryPath());
+	}
+
+	// Create an instance of the Gatherer class, which will parse the args and prepare the rest of the GLI
+	gatherer = new Gatherer(args);
+
+	// Create a button for the user to press to open the GLI GUI
+	JButton launch_GLI_button = new JButton("Launch Greenstone Librarian Interface ...");
+	launch_GLI_button.addActionListener(this);
+	getContentPane().add(launch_GLI_button);
+    }
+
+
+    public void start() 
+    {
+	System.err.println("Start called");
+    }
+
+
+    public void stop()
+    {
+	System.err.println("Stop called");
+    }
+
+
+    public void destroy()
+    {
+	System.err.println("Destroy called");
+	gatherer.exit();
+	gatherer = null;
+	System.err.println("Done gatherer exit.");
+    }
+
+
+    public void actionPerformed(ActionEvent e)
+    {
+	gatherer.openGUI();
+    }
+
+
+    /** 
+     * Method which unzips a given metadata resource.
+     * @param jar_zip_fname The name of the jar file as a <strong>String</strong>.
+     * @param dst_dir The destination directory for unzipping, also as a <strong>String</strong>.
+     * @see JarTools.extractFromJar(String, String, boolean)
+     */
+    static public void unzipFromJar(String jar_zip_fname, String dst_dir)
+    {
+	if (!dst_dir.endsWith(File.separator)) {
+	    dst_dir += File.separator;
+	}
+
+	JarTools.extractFromJar(jar_zip_fname, dst_dir, true);
+	UnzipTools.unzipFile(dst_dir + jar_zip_fname, dst_dir);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GathererApplet4gs3.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GathererApplet4gs3.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GathererApplet4gs3.java	(revision 31635)
@@ -0,0 +1,203 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer;
+
+import java.applet.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.net.*;
+import javax.swing.JApplet;
+
+import java.io.*;
+import javax.swing.*;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.UnzipTools;
+import org.greenstone.gatherer.util.Utility;
+
+
+public class GathererApplet4gs3 extends JApplet implements ActionListener
+{
+    private Gatherer gatherer = null;
+    private String gsdl3_string = "";
+    private String library_url_string = "";
+    private String gliserver_url_string = "";
+
+    protected String fullLibraryURL(String address) 
+    {
+	String full_address = "";
+
+        // make sure the URL has protocol, host, and file
+        if (address.startsWith("http:")) {
+            // the address has all the necessary components
+	    full_address = address;
+        } 
+	else if (address.startsWith("/")) {
+            // there is not protocol and host
+            URL document = getDocumentBase();
+	    int port_no = document.getPort();
+
+            String port = (port_no>0) ? ":" + port_no : "";
+            full_address = "http://" + document.getHost() + port  + address;
+	}
+
+	return full_address;
+    }
+
+
+    /* This is the entry point for the GLI applet */
+    public void init()
+    {
+	JarTools.initialise(this); 
+
+	// Check if the user has agreed to the requested security settings
+	try {
+	    // Work-around to reduce number of 
+	    // 'Could not lock user prefs' error messages.
+	    // Thanks to Walter Schatz from the java forums.
+	    System.setProperty("java.util.prefs.syncInterval","2000000");
+	}
+	catch (Exception exception) {
+	    getContentPane().add(new JLabel("Greenstone Librarian Interface Applet deactivated", JLabel.CENTER));
+	    return;
+	}
+
+	// Ensure platform specific LAF
+	try {
+	    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+	}
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	}
+
+	// Determine the GLI user directory path
+	String gli_user_directory_path = System.getProperty("user.home") + File.separator;
+        if (Utility.isWindows()) {
+	    String windows_home_parameter = getParameter("windowsHome");
+	    if (windows_home_parameter != null && !windows_home_parameter.equals("")) {
+		gli_user_directory_path = windows_home_parameter + File.separator + "GLI" + File.separator;
+	    }
+	    else {
+		gli_user_directory_path += "Application Data" + File.separator + "Greenstone" + File.separator + "GLI" + File.separator;
+	    }
+	}
+        else {
+            gli_user_directory_path += ".gli" + File.separator;
+        }
+	Gatherer.setGLIUserDirectoryPath(gli_user_directory_path);
+
+	// If we're running as an applet we don't have a local GLI, so use the user directory path as the GLI path
+	Gatherer.setGLIDirectoryPath(gli_user_directory_path);
+
+	// Set some global variables so we know how we're running
+	Gatherer.isApplet = true;
+	String library_url_string = fullLibraryURL(getParameter("gwcgi"));
+	String gliserver_url_string = library_url_string + "/cgi-bin/gliserver.pl";
+	String[] args = { "-use_remote_greenstone",
+			  "-gliserver_url", gliserver_url_string,
+			  "-library_url", library_url_string,
+			  "-gsdl3",
+			  //"-debug",
+	};
+
+	File collect_directory = new File(Gatherer.getCollectDirectoryPath());
+	if (!collect_directory.exists()) {
+	    if (JarTools.isInJar("collect.zip")) {
+		// Dig out collect.zip from JAR file and unzip it (thereby
+		// creating collect directory and descendants.
+		//   This is done to seed gsdl home folder with config files
+		//   for documented example collections so "base this on
+		//   existing collection" works
+		unzipFromJar("collect.zip", Gatherer.getGLIUserDirectoryPath());
+	    }
+	    else {
+		// Prepare an empty collect dir for use
+		if (!collect_directory.mkdir()) {
+		    System.err.println("Warning: Unable to make directory: " + collect_directory);
+		}
+	    }
+	}
+
+	File metadata_directory = new File(Gatherer.getGLIMetadataDirectoryPath());
+	if (!metadata_directory.exists()) {
+	    // dig out metadata.zip from JAR file and unzip it
+	    unzipFromJar("metadata.zip", Gatherer.getGLIDirectoryPath());
+	}
+
+	// Create an instance of the Gatherer class, which will parse the args and prepare the rest of the GLI
+	gatherer = new Gatherer(args);
+	// Create a button for the user to press to open the GLI GUI
+	JButton launch_GLI_button = new JButton("Launch Greenstone Librarian Interface ...");
+	launch_GLI_button.addActionListener(this);
+	getContentPane().add(launch_GLI_button);
+    }
+
+
+    public void start() 
+    {
+	System.err.println("Start called");
+    }
+
+
+    public void stop()
+    {
+	System.err.println("Stop called");
+    }
+
+
+    public void destroy()
+    {
+	System.err.println("Destroy called");
+	gatherer.exit();
+	// bug that causes browser (Firefox to freeze) when trying to reload the applet
+	// gatherer = null;
+	System.err.println("Done gatherer exit.");
+    }
+
+
+    public void actionPerformed(ActionEvent e)
+    {
+	gatherer.openGUI();
+    }
+
+
+    /** 
+     * Method which unzips a given metadata resoure.
+     * @param jar_zip_fname The name of the jar file as a <strong>String</strong>.
+     * @param dst_dir The destination directory for unzipping, also as a <strong>String</strong>.
+     * @see JarTools.extractFromJar(String, String, boolean)
+     */
+    static public void unzipFromJar(String jar_zip_fname, String dst_dir)
+    {
+	if (!dst_dir.endsWith(File.separator)) {
+	    dst_dir += File.separator;
+	}
+
+	JarTools.extractFromJar(jar_zip_fname, dst_dir, true);
+	UnzipTools.unzipFile(dst_dir + jar_zip_fname, dst_dir);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GathererProg.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GathererProg.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GathererProg.java	(revision 31635)
@@ -0,0 +1,83 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, NZDL Project, University of Waikato
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer;
+
+
+import java.io.*;
+import javax.swing.*;
+import org.greenstone.gatherer.util.Utility;
+
+
+/**
+ * Containing the main() method, this class is the entry point for the
+ * stand-alone (non-Applet) GLI. The main point of this class is to set
+ * some stand-alone specific properties and then create an instance of the
+ * Gatherer class.
+ * @author John Thompson, NZDL Project, University of Waikato
+ */
+public class GathererProg
+{
+    /** The entry point into the stand-alone GLI.
+     * @param args A collection of arguments that may include: initial screen size, dictionary, path to the GSDL etc.
+     */
+    static public void main(String[] args)
+    {
+	// A serious hack, but its good enough to stop crappy 'Could not
+	// lock user prefs' error messages.  Thanks to Walter Schatz from
+	// the java forums.
+	System.setProperty("java.util.prefs.syncInterval","2000000"); 
+	// => results in one message every 600 hours!
+
+	// Ensure platform specific LAF
+	try {
+	    // UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+	    UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
+	}
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	}
+
+	// Determine the GLI user directory path
+	String gli_user_directory_path = System.getProperty("user.home") + File.separator;
+        if (Utility.isWindows()) {
+	    gli_user_directory_path += "Application Data" + File.separator + "Greenstone" + File.separator + "GLI" + File.separator;
+        }
+        else {
+            gli_user_directory_path += ".gli" + File.separator;
+        }
+	Gatherer.setGLIUserDirectoryPath(gli_user_directory_path);
+
+	// We have a local GLI
+	Gatherer.setGLIDirectoryPath(System.getProperty("user.dir") + File.separator);
+
+	// Create an instance of the Gatherer class, which will parse the args and prepare the rest of the GLI
+	Gatherer gatherer = new Gatherer(args);
+
+	// Display the GUI immediately
+	gatherer.openGUI();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GetOpt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GetOpt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/GetOpt.java	(revision 31635)
@@ -0,0 +1,325 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson and David Bainbridge, 
+ *         Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer;
+
+import java.io.*;
+import org.greenstone.gatherer.util.StaticStrings; 
+import org.greenstone.gatherer.util.Utility;
+
+public class GetOpt
+{
+    public boolean debug = false; 
+    public boolean feedback_enabled = false;
+    public boolean no_load = false;
+    public boolean use_remote_greenstone = false;
+    public boolean new_set = false;
+    public boolean run_gsdl3 = false;
+
+    public String client_operating_system = null;
+    public String collect_directory_path = null;
+    public String filename = null;
+    public String gliserver_url_string = null;
+    public String gsdl_path = null;
+    public String gsdl3_path = null;
+    public String gsdl3_src_path = null;
+    public String library_url_string = null;
+    public String local_library_path = null;
+    public String perl_path = null;
+    public String site_name = null; // for GS3
+    public String servlet_path = null;
+    public String metadata_path = null;
+
+    protected FedoraInfo fedora_info = null;
+    
+    public GetOpt(String[] args)
+    {
+	// Default dictionary. Only used for starting error messages.
+	Dictionary dictionary = new Dictionary(null, null); 
+
+	fedora_info = new FedoraInfo();
+
+	// Parse arguments
+	int argument_index = 0;
+	String next_token = null;
+
+	while(argument_index < args.length || next_token != null) {
+	    // 1. We start by attempting to parse an argument name. An argument
+	    //    must start with a '-', and should not contain spaces. If
+	    //    anything else is encountered it is ignored.
+
+	    String argument_name = null;
+	    if(next_token == null) {
+		next_token = args[argument_index];
+		argument_index++;
+	    }
+
+	    if(next_token.startsWith(StaticStrings.MINUS_CHARACTER)) {
+		// Trim second '-' just to be kind to Unixy-type people
+		if(next_token.startsWith(StaticStrings.MINUS_CHARACTER + StaticStrings.MINUS_CHARACTER)) {
+		    argument_name = next_token.substring(1);
+		}
+		else {
+		    argument_name = next_token;
+		}
+	    }
+	    next_token = null;
+	    // 2. If we now have an argument name we continue by attempting
+	    //    to parse a value. A value is taken to be the sequence of
+	    //    space seperated Strings between the last argument name
+	    //    and up to but not including the next argument name. Of
+	    //    course an argument needn't have any value (ie -debug,
+	    //    -help), in which case value will be null.
+	    if(argument_name != null) {
+		String argument_value = null;
+		StringBuffer argument_value_buffer = new StringBuffer("");
+		while(argument_index < args.length && next_token == null) {
+		    next_token = args[argument_index];
+		    argument_index++;
+		    // If we just parsed an arbitary String then append it to value, followed by a single space
+		    if(!next_token.startsWith(StaticStrings.MINUS_CHARACTER)) {
+			argument_value_buffer.append(next_token);
+			argument_value_buffer.append(StaticStrings.SPACE_CHARACTER);
+			next_token = null;
+		    }
+		    // If the argument token retrieved is an argument name,
+		    // then leave it in next_token, which will cause the
+		    // argument parsing process to move onto the next step.
+		}
+		// If a value now exists in argument buffer, retrieve
+		// it. Remove the last character as it will be an erroneous
+		// space.
+		if(argument_value_buffer.length() > 0) {
+		    argument_value = argument_value_buffer.substring(0, argument_value_buffer.length() - 1);
+		}
+		
+		// 3. We now have the argument name, and any associated
+		//    value. We are ready to store the data in the
+		//    appropriate variables.
+		DebugStream.println("Parsed Argument: name=" + argument_name + (argument_value != null ? (", value=" + argument_value) : ", no value"));
+		// 3a. First those arguments that have no associated value
+		if(argument_value == null) {
+		    if(argument_name.equals(StaticStrings.HELP_ARGUMENT)) {
+			System.out.println(Dictionary.get("General.Usage"));
+			System.exit(0);
+		    }
+		    // Run GLI in debug mode. Produces debug log plus extra
+		    // messages.
+		    else if(argument_name.equals(StaticStrings.DEBUG_ARGUMENT)) {
+			debug = true;
+		    }
+		    // Run GLI with feedback enabled. 
+		    else if(argument_name.equals(StaticStrings.FEEDBACK_ARGUMENT)) {
+			feedback_enabled = true;
+		    }
+		    // Forces no loading on previous collection.
+		    else if(argument_name.equals(StaticStrings.NO_LOAD_ARGUMENT)) {
+			no_load = true;
+			filename = null;
+		    }
+		    // Use a remote Greenstone rather than a local one
+		    else if (argument_name.equals(StaticStrings.USE_REMOTE_GREENSTONE_ARGUMENT)) {
+			use_remote_greenstone = true;
+			//Use a remote Greenstone
+		    }
+		    else if (argument_name.equals(StaticStrings.GSDL3_ARGUMENT)){
+			//Use a remote Greenstone3
+			run_gsdl3=true;
+		    }
+		    else if (argument_name.equals(StaticStrings.NEW_METADATASET)) {
+			new_set = true;
+		    }
+		    else if(argument_name.equals(StaticStrings.FEDORA_MODE)) {
+			// Running FLI remotely
+			fedora_info.setActive(true);
+		    }
+		}
+		// 3b. Now for those that do
+		else {
+		    // Parse the path to the GSDL. Required argument.
+		    if(argument_name.equals(StaticStrings.GSDL_ARGUMENT)) {
+			if(argument_value.endsWith(File.separator)) {
+			    gsdl_path = argument_value;
+			}
+			else {
+			    gsdl_path = argument_value + File.separator;
+			}
+		    }
+		    // GSDL3 path
+		    if(argument_name.equals(StaticStrings.GSDL3_ARGUMENT)) {
+			if(argument_value.endsWith(File.separator)) {
+			    gsdl3_path = argument_value;
+			}
+			else {
+			    gsdl3_path = argument_value + File.separator;
+			}
+		    }
+		    // GSDL3 src path
+		    if(argument_name.equals(StaticStrings.GSDL3_SRC_ARGUMENT)) {
+			if(argument_value.endsWith(File.separator)) {
+			    gsdl3_src_path = argument_value;
+			}
+			else {
+			    gsdl3_src_path = argument_value + File.separator;
+			}
+		    }
+		    // Client operating system
+		    else if (argument_name.equals(StaticStrings.GSDLOS_ARGUMENT)) {
+			client_operating_system = argument_value;
+		    }
+		    
+		    else if (argument_name.equals(StaticStrings.SITE_ARGUMENT)) {
+			site_name = argument_value;
+		    }
+		    else if (argument_name.equals(StaticStrings.SERVLET_ARGUMENT)) {
+			servlet_path = argument_value;
+		    }
+		    // Specify a non-standard collect directory to use (for running one GLI in a network environment)
+		    else if (argument_name.equals(StaticStrings.COLLECTDIR_ARGUMENT)) {
+			collect_directory_path = argument_value;
+			System.err.println("Non standard collect directory specified: " + collect_directory_path);
+		    }
+		    // Specify a collection to load initially. Could be used for file associations.
+		    else if(argument_name.equals(StaticStrings.LOAD_ARGUMENT)) {
+			filename = argument_value;
+			no_load = false;
+		    }
+		    // Parse the file path of the local library server
+		    else if (argument_name.equals(StaticStrings.LOCAL_LIBRARY_ARGUMENT)) {
+			local_library_path = argument_value;
+		    }
+		    // Manually specify the Greenstone library URL
+		    else if (argument_name.equals(StaticStrings.LIBRARY_URL_ARGUMENT)) {
+			library_url_string = argument_value;
+		    }
+		    // Specify the URL to the gliserver CGI script for remote collection building
+		    else if (argument_name.equals(StaticStrings.GLISERVER_URL_ARGUMENT)) {
+			gliserver_url_string = argument_value;
+		    }
+		    // Parse the path to PERL. If not provided it assumes
+		    // perl should be available on the PATH.
+		    else if(argument_name.equals(StaticStrings.PERL_ARGUMENT)) {
+			perl_path = argument_value;
+			// Test whether this points to the Perl bin
+			// directory or the Perl executable itself.
+			File perl_file = new File(perl_path);
+			if(perl_file.isDirectory()) {
+			    // If this is windows we create a child file
+			    // perl.exe, otherwise we create perl
+			    if(Utility.isWindows()) {
+				perl_file = new File(perl_file, Utility.PERL_EXECUTABLE_WINDOWS);
+			    }
+			    else {
+				perl_file = new File(perl_file, Utility.PERL_EXECUTABLE_UNIX);
+			    }
+			    // And store this new path.
+			    perl_path = perl_file.getAbsolutePath();
+			    perl_file = null;
+			}
+			// Otherwise it is fine as it is
+		    }
+                    else if(argument_name.equals(StaticStrings.METADATA_PATH)){
+			if (argument_value != null && !argument_value.equals("")) {
+			    File metadata_file = new File(argument_value);
+			    if (metadata_file.exists()) {
+				metadata_path = argument_value;
+			    } 
+			}
+		    }
+
+		    // Fedora home - when running Fedora locally
+		    if(argument_name.equals(StaticStrings.FEDORA_HOME)) {
+			if(argument_value.endsWith(File.separator)) {
+			    fedora_info.setHome(argument_value);
+			}
+			else {
+			    fedora_info.setHome(argument_value + File.separator);
+			}
+		    }
+
+		    if(argument_name.equals(StaticStrings.FEDORA_VERSION)) {
+			fedora_info.setVersion(argument_value);
+		    }
+		    
+		    // Fedora hostname
+		    if(argument_name.equals(StaticStrings.FEDORA_HOSTNAME)) {
+			if(argument_value.endsWith(File.separator)) {
+			    fedora_info.setHostname(argument_value);
+			}
+			else {
+			    fedora_info.setHostname(argument_value + File.separator);
+			}
+		    }
+		    // Fedora port
+		    if(argument_name.equals(StaticStrings.FEDORA_PORT)) {
+			if(argument_value.endsWith(File.separator)) {
+			    fedora_info.setPort(argument_value);
+			}
+			else {
+			    fedora_info.setPort(argument_value + File.separator);
+			}
+		    }
+		    // Fedora username
+		    if(argument_name.equals(StaticStrings.FEDORA_USERNAME)) {
+			if(argument_value.endsWith(File.separator)) {
+			    fedora_info.setUsername(argument_value);
+			}
+			else {
+			    fedora_info.setUsername(argument_value + File.separator);
+			}
+		    }
+		    // Fedora password
+		    if(argument_name.equals(StaticStrings.FEDORA_PASSWORD)) {
+			if(argument_value.endsWith(File.separator)) {
+			    fedora_info.setPassword(argument_value);
+			}
+			else {
+			    fedora_info.setPassword(argument_value + File.separator);
+			}
+		    }
+		    // Fedora protocol, e.g. http or https
+		    if(argument_name.equals(StaticStrings.FEDORA_PROTOCOL)) {
+			if(argument_value.endsWith(File.separator)) {
+			    fedora_info.setProtocol(argument_value);
+			}
+			else {
+			    fedora_info.setProtocol(argument_value + File.separator);
+			}
+		    }
+
+
+
+
+                  
+		}
+	    }
+	    // Argument name was null, nothing to be done.
+	}
+	next_token = null;
+	// Arguments all parsed.
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Argument.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Argument.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Argument.java	(revision 31635)
@@ -0,0 +1,648 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Greenstone Librarian Interface (GLI) application, 
+ * part of the Greenstone digital library software suite from the New 
+ * Zealand Digital Library Project at the University of Waikato, 
+ * New Zealand.
+ *
+ * Author: John Thompson 
+ *         Greenstone Project, New Zealand Digital Library
+ *         University of Waikato
+ *         http://www.nzdl.org
+ *
+ * Copyright (C) 2004 New Zealand Digital Library, University of Waikato
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.cdm;
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.Collection;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+/** This class contains all the details about a single argument that can be passed to this plugin, including option lists if the parameters are restricted.
+ * @author John Thompson, Greenstone Project, New Zealand Digital Library, University of Waikato
+ * @version 2.41 final
+ */
+public class Argument
+  implements Comparable, Serializable {
+  /** An element of the argument type enumeration specifying a combobox control. */
+  static final public byte ENUM = 0;
+  /** An element of the argument type enumeration specifying a checkbox control. */
+  static final public byte FLAG = 1;
+  /** An element of the argument type enumeration specifying a tree control. */
+  static final public byte HIERARCHY = 2;
+  /** An element of the argument type enumeration specifying a spinner control. */
+  static final public byte INTEGER = 3;
+  /** An element of the argument type enumeration specifying a language combobox control. */
+  static final public byte LANGUAGE = 4;
+  /** An element of the argument type enumeration specifying a metadata combobox control. */
+  static final public byte METADATA = 5;
+  /** An element of the argument type enumeration specifying a text field. */
+  static final public byte STRING = 6;
+  /** An element of the argument type enumeration specifying a regular expression text field. */
+  static final public byte REGEXP = 7;
+  /** An element of the argument type enumeration specifying a metadata set combobox control. */
+  static final public byte METADATA_SET_NAMESPACE = 8;
+  /** An element of the argument type enumeration specifying a text field. */
+  static final public byte URL = 9;
+  /** An editable combo box */
+  static final public byte ENUM_STRING = 10;
+
+  /////////////////////////////////////////////////////////////////
+
+  /** true if this argument should actually be hidden within the GLI. This is important for arguments such as import dir or other location critical arguments. */
+  private boolean hidden_gli = false;
+  /** <i>true</i> if this argument is required for the applicable script to work properly, <i>false</i> otherwise. */
+  private boolean required = false;
+  /** The type of this argument. Used to be an int, but bytes are cheaper. */
+  private byte type = STRING;
+  /** The maximum value an integer based control can have. */
+  private int maximum = Integer.MAX_VALUE;
+  /** The minimum value an integer based control can have. */
+  private int minimum = Integer.MIN_VALUE;
+  /** Every argument has a detail mode level at which it becomes available to the user to edit. 
+   * @see org.greenstone.gatherer.Configuration
+   */
+  private int mode_level = Configuration.LIBRARIAN_MODE;
+  /** The DOM element this argument is built around, if any. */
+  private Element element;
+  /** If the argument is of type ENUM or ENUM_STRING then this map holds all the various options. Each entry is an &lt;option value&gt; -&gt; &lt;description&gt; mapping. */
+  private ArrayList option_list = null;
+  /** A default value for parameter-type arguments. May be a Perl pattern. */
+  private String default_value = null;
+  /** The text description of this argument parsed from the pluginfo output. */
+  private String description = null;
+  /** The argument flag as it appears in the command. Also used as the unique identifier of an argument. */
+  private String name = null;
+    /** The value of the arg, stored for metadata type options */
+    private String stored_value = null;
+  /** The plugin that owns this argument, for the purposes of visualising inheritance. */
+  private String owner = null;
+
+  private String display_name = null;
+
+  /** Default Constructor. */
+  public Argument() {
+  }
+
+  /** Another constructor but this one is a little more interesting as it takes a DOM element.
+   * @param element the Element this argument is based around
+   */
+  public Argument(Element element) {
+    this.element = element;
+  }
+
+  /** Method to add an element to the option_list.
+   * @param name the name value of the option as a String
+   * @param desc the description of this options as a String
+   */
+  public void addOption(String name, String desc) {
+    if((type == ENUM || type == ENUM_STRING) && name != null) {
+      if(desc == null) {
+	desc = "";
+      }
+      if(option_list == null) {
+	option_list = new ArrayList();
+      }
+      option_list.add(new ArgumentOption(name, desc));
+    }
+  }
+
+  /** Method to compare two arguments for ordering.
+   * @param object the argument we are comparing to, as an Object
+   * @return an int specifying the argument order, using values as set out in String
+   * @see org.greenstone.gatherer.cdm.Argument
+   */
+  public int compareTo(Object object) {
+    if(object instanceof Argument) {
+      return getName().compareTo(((Argument)object).getName());
+    }
+    else {
+      return toString().compareTo(object.toString());
+    }
+  }
+
+  /** Create a copy of this argument.
+   * @return a newly created Argument with the same details as this one
+   */
+  public Argument copy() {
+    Argument copy = new Argument();
+    copy.setDefaultValue(default_value);
+    copy.setDescription(description);
+    copy.setOptions(option_list);
+    copy.setOwner(owner);
+    copy.setName(name);		
+    copy.setDisplayName(display_name);
+    copy.setRequired(required);
+    copy.setType(type);
+    copy.setMinimum(minimum);
+    copy.setMaximum(maximum);
+    copy.setModeLevel(mode_level);
+    copy.setHiddenGLI(hidden_gli);
+    return copy;
+  }
+
+  /** Method to determine if two arguments are equal.
+   * @param object the argument to test against, as an Object
+   * @return true if the arguments names match, false otherwise
+   */
+  public boolean equals(Object object) {
+    return (compareTo(object) == 0);
+  }
+
+  /** Method to retrieve the value of default_value.
+   * @return a String containing the default value
+   */
+  public String getDefaultValue() {
+    return default_value;
+  }
+
+  /** Method to retrieve this arguments description.
+   * @return a String containing the description
+   */
+  public String getDescription() {
+    return description;
+  }
+
+  public Element getElement() {
+    return element;
+  }
+  /** Retrieve the upper bound of a range based argument.
+   * @return the maximum as an int
+   */
+  public int getMaximum() {
+    return maximum;
+  }
+
+  /** Retrieve the lower bound of a range based argument.
+   * @return the minimum as an int
+   */
+  public int getMinimum() {
+    return minimum;
+  }
+
+  /** Retrieves the mode level at which this argument should become available. Any higher levels should also see this argument.
+   * @return the mode level as an int
+   */
+  public int getModeLevel() {
+    return mode_level;
+  }
+
+  /** Method to retrieve the value of name.
+   * @return a String containing the argument name
+   * @see org.greenstone.gatherer.util.StaticStrings#NAME_ATTRIBUTE
+   */
+  public String getName() {
+    if(name == null && element != null) {
+      name = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+    }
+    return name;
+  }
+
+  public String getDisplayName() {
+    if(display_name==null)
+      return "";
+    return display_name;
+  }
+
+  /** Method to retrieve the option list for this argument.
+   * @return a HashMap containing &lt;option value&gt; -&gt; &lt;description&gt; entries
+   */
+  public ArrayList getOptions() {
+    return option_list;
+  }
+
+  /** Retrieve the name of the owner of this argument.
+   * @return the owners name as a String
+   */
+  public String getOwner() {
+    return owner;
+  }
+
+  /** Method to determine the type of this argument.
+   * @return a byte specifying the type
+   */
+  public byte getType() {
+    return type;
+  }
+
+  /** Method to retrieve the value of value.
+   * @return the value of value as a String
+   * @see org.greenstone.gatherer.Gatherer#c_man
+   * @see org.greenstone.gatherer.collection.CollectionManager#getCollection
+   */
+  public String getValue()
+  {
+    // Only assigned arguments have values.
+    if (element == null) {
+      return null;
+    }
+    if (stored_value != null) {
+	return stored_value;
+    }
+    String value = XMLTools.getValue(element);
+    if (type == METADATA) {
+	// We display using metadata display name, but store in the XML using
+	// full name (canonical name)
+	stored_value = MetadataTools.convertMetadataElementListNames(value, MetadataTools.TO_DISPLAY_NAMES);
+	return stored_value;
+    }
+    return value; 
+  }
+
+
+  /** Method to determine if this argument has been assigned.
+   * @return true if it has, false otherwise
+   * @see org.greenstone.gatherer.util.StaticStrings#ASSIGNED_ATTRIBUTE
+   * @see org.greenstone.gatherer.util.StaticStrings#TRUE_STR
+   */
+  public boolean isAssigned() {
+    return (element != null && element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.TRUE_STR));
+  }
+
+  /** Determine if this is a custom argument ie one that has been parsed from the config file but doesn't have a matching entry in the argument library.
+   * @return true if this argument is a custom, false otherwise
+   * @see org.greenstone.gatherer.util.StaticStrings#CUSTOM_ATTRIBUTE
+   * @see org.greenstone.gatherer.util.StaticStrings#TRUE_STR
+   */
+  public boolean isCustomArgument() {
+    return (element != null && element.getAttribute(StaticStrings.CUSTOM_ATTRIBUTE).equals(StaticStrings.TRUE_STR));
+  }
+
+  /** Determine if this argument is hidden in GLI
+   * @return true if the argument is hidden, false otherwise
+   */
+  public boolean isHiddenGLI() {
+    return hidden_gli;
+  }
+
+  /** Method to determine of this argument is required for the associated script to work.
+   * @return true if this argument is required, false otherwise
+   */
+  public boolean isRequired() {
+    return required;
+  }
+
+  /** Method to allow for the activation of arguments that might never have their setValue() method called.
+   * @param assigned the desired state as a boolean
+   * @see org.greenstone.gatherer.util.StaticStrings#ASSIGNED_ATTRIBUTE
+   * @see org.greenstone.gatherer.util.StaticStrings#FALSE_STR
+   * @see org.greenstone.gatherer.util.StaticStrings#TRUE_STR
+   */
+  public void setAssigned(boolean assigned) {
+    if(element != null) {
+      element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+    }
+  }
+
+  /** Sets the value of default_value.
+   * @param default_value The new value for default_value as a <strong>String</strong>.
+   */
+  public void setDefaultValue(String default_value) {
+    this.default_value = default_value;
+  }
+
+  /** Set the value of desc.
+   * @param description the new value of desc as a String
+   */
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  /** Set the element this argument should be based upon.
+   * @param element the Element
+   */
+  public void setElement(Element element) {
+    this.element = element;
+  }
+
+  /** Mark this argument as being hidden in GLI. */
+  public void setHiddenGLI(boolean hidden) {
+    this.hidden_gli = hidden;
+  }
+
+  /** Set the upper bound for a range type argument.
+   * @param maximum the maximum as an int
+   */
+  public void setMaximum(int maximum) {
+    this.maximum = maximum;
+  }
+
+  /** Set the lower bound for a range type argument.
+   * @param minimum the minimum as an int
+   */
+  public void setMinimum(int minimum) {
+    this.minimum = minimum;
+  }
+
+  /** Set the detail mode level where this argument will become available.
+   * @param mode_level the mode level as an int
+   */
+  public void setModeLevel(int mode_level) {
+    this.mode_level = mode_level;
+  }
+
+  /** Set the value of name.
+   * @param name the new value of name as a String
+   */
+  public void setName(String name) {
+    this.name = name;
+  }
+	
+  public void setDisplayName(String name) {		
+    this.display_name = name; 
+  }	
+
+  /** Sets the value of the options list.
+   * @param list the new options list as a HashMap
+   */
+  public void setOptions(ArrayList list) {
+    this.option_list = list;
+  }
+
+  /** Set the owner of this argument.
+   * @param owner the name of the owner of this argument as a String
+   */
+  public void setOwner(String owner) {
+    this.owner = owner;
+  }
+
+  /** Set the value of required.
+   * @param required the new value of required as a boolean
+   */
+  public void setRequired(boolean required) {
+    this.required = required;
+  }
+
+  /** Set the value of type.
+   * @param type the new value of type as an byte
+   */
+  public void setType(byte type) {
+    this.type = type;
+  }
+
+  /** Set the value of type, by matching a type to the given string.
+   * @param new_type a String which contains the name of a certain argument type
+   * @see org.greenstone.gatherer.util.StaticStrings#ENUM_STR
+   * @see org.greenstone.gatherer.util.StaticStrings#FLAG_STR
+   * @see org.greenstone.gatherer.util.StaticStrings#HIERARCHY_STR
+   * @see org.greenstone.gatherer.util.StaticStrings#INT_STR
+   * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGE_STR
+   * @see org.greenstone.gatherer.util.StaticStrings#METADATA_TYPE_STR
+   * @see org.greenstone.gatherer.util.StaticStrings#REGEXP_STR
+   */
+  public void setType(String new_type) {
+    if(new_type.equalsIgnoreCase(StaticStrings.ENUM_STR)) {
+      this.type = ENUM;
+      option_list = new ArrayList();
+    }
+    else if(new_type.equalsIgnoreCase(StaticStrings.ENUM_STRING_STR)) {
+      this.type = ENUM_STRING;
+      option_list = new ArrayList();
+    }
+    else if(new_type.equalsIgnoreCase(StaticStrings.FLAG_STR)) {
+      this.type = FLAG;
+    }
+    else if(new_type.equalsIgnoreCase(StaticStrings.HIERARCHY_STR)) {
+      this.type = HIERARCHY;
+    }
+    else if(new_type.equalsIgnoreCase(StaticStrings.INT_STR)) {
+      this.type = INTEGER;
+    }
+    else if(new_type.equalsIgnoreCase(StaticStrings.LANGUAGE_STR)) {
+      this.type = LANGUAGE;
+    }
+    else if(new_type.equalsIgnoreCase(StaticStrings.METADATA_TYPE_STR)) {
+      this.type = METADATA;
+    }
+    else if(new_type.equalsIgnoreCase(StaticStrings.REGEXP_STR)) {
+      this.type = REGEXP;
+    }
+    else {
+      this.type = STRING;
+    }
+  }
+
+  /** Method to set the value of this argument.
+   * @param value the new value for the argument
+   * @see org.greenstone.gatherer.Gatherer#println
+   */
+  public void setValue(String value) {
+    if(element != null) {
+	if (type == METADATA) {
+	    value = MetadataTools.convertMetadataElementListNames(value, MetadataTools.FROM_DISPLAY_NAMES);
+	    stored_value = value;
+	}
+	XMLTools.setValue(element, value);
+    }
+    else {
+      DebugStream.println("Argument.setValue(" + value + ") called on a base Argument.");
+    }
+  }
+
+  /** Set the values vector to the given values. Currently I just assign the new values, whereas I may later want to implement a deep clone.
+   * @param values an ArrayList of values
+   * @see org.greenstone.gatherer.Gatherer#println
+   */
+  public void setValues(ArrayList values) {
+    if(element != null) {
+      StringBuffer value = new StringBuffer();
+      int value_length = values.size();
+      for(int i = 0; i < value_length; i++) {
+	value.append(values.get(i));
+	value.append(StaticStrings.COMMA_CHARACTER);
+      }
+      value.deleteCharAt(value.length() - 1); // Remove last ','
+      XMLTools.setValue(element, value.toString());
+    }
+    else {
+      DebugStream.println("Argument.setValues([" + values.size() + " items]) called on a base Argument.");
+    }
+  }
+
+  /** Method for translating the data of this class into a string.
+   * @return a String containing a fragment of the total arguments string
+   * @see org.greenstone.gatherer.Gatherer#c_man
+   * @see org.greenstone.gatherer.collection.CollectionManager#getCollection
+   * @see org.greenstone.gatherer.util.StaticStrings#COMMA_CHARACTER
+   * @see org.greenstone.gatherer.util.StaticStrings#NAME_ATTRIBUTE
+   * @see org.greenstone.gatherer.util.StaticStrings#SPACE_CHARACTER
+   * @see org.greenstone.gatherer.util.StaticStrings#SPEECH_CHARACTER
+   */
+  public String toString()
+  {
+    StringBuffer text = new StringBuffer("-");
+
+    if (element == null) {
+      return text.toString();
+    }
+
+    if (name == null) {
+      name = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+    }
+    text.append(name);
+
+    // getValue returns display name for metadata args
+    String value = getValue(); 
+    if (value.length() == 0) {
+      return text.toString();
+    }
+
+    text.append(StaticStrings.SPACE_CHARACTER);
+
+    // If the value contains a space, add speech marks
+    //   (Except for metadata elements, which won't have spaces when written out to collect.cfg)
+    if (value.indexOf(StaticStrings.SPACE_CHARACTER) != -1 && type != METADATA) {
+      value = StaticStrings.SPEECH_CHARACTER + value + StaticStrings.SPEECH_CHARACTER;
+    }
+
+    text.append(value);
+    return text.toString();
+  } 
+
+  /** parse the <Option> XML from eg import.pl -xml or pluginfo.pl -xml */
+  public void parseXML(Element option) {
+
+    for(Node node = option.getFirstChild(); node != null; node = node.getNextSibling()) {
+      String node_name = node.getNodeName();
+      if(node_name.equals("Name")) {
+	setName(XMLTools.getValue(node));
+      }
+      else if(node_name.equals("DisplayName")) {
+	setDisplayName(XMLTools.getValue(node));
+      }
+      else if(node_name.equals("Desc")) {
+	setDescription(XMLTools.getValue(node));
+      }
+      else if(node_name.equals("Type")) {
+	setType(XMLTools.getValue(node));
+      }
+      else if(node_name.equals("Default")) {
+	setDefaultValue(XMLTools.getValue(node));
+      }
+      else if(node_name.equals("Required")) {
+	String v = XMLTools.getValue(node);
+	if(v != null && v.equals("yes")) {
+	  setRequired(true);
+	}
+      }
+      else if(node_name.equals("List")) {
+	// Two final loops are required to parse lists.
+	for(Node value = node.getFirstChild(); value != null; value = value.getNextSibling()) {
+	  if(value.getNodeName().equals("Value")) {
+	    String key = null;
+	    String desc = "";
+	    for(Node subvalue = value.getFirstChild(); subvalue != null; subvalue = subvalue.getNextSibling()) {
+	      node_name = subvalue.getNodeName();
+	      if(node_name.equals("Name")) {
+		key = XMLTools.getValue(subvalue);
+	      }
+	      else if(node_name.equals("Desc")) {
+		desc = XMLTools.getValue(subvalue);
+	      }
+	    }
+	    if(key != null) {
+	      addOption(key, desc);
+	    }
+	  }
+	}
+      }
+      else if(node_name.equals("Range")) {
+	String range_raw = XMLTools.getValue(node);
+	int index = -1;
+	if((index = range_raw.indexOf(StaticStrings.COMMA_CHARACTER)) != -1) {
+	  if(index > 0) {
+	    try {
+	      String first_number = range_raw.substring(0, index);
+	      setMinimum(Integer.parseInt(first_number));
+	      first_number = null;
+	    }
+	    catch(Exception exception) {
+	    }
+	  }
+
+	  if(index + 1 < range_raw.length()) {
+	    try {
+	      String second_number = range_raw.substring(index + 1);
+	      setMaximum(Integer.parseInt(second_number));
+	      second_number = null;    
+	    }
+	    catch(Exception exception) {
+	    }
+	  }
+	}
+	// Else it wasn't a valid range anyway, so ignore it
+      }
+      else if(node_name.equals("HiddenGLI")) {
+	setHiddenGLI(true);
+      }
+      else if(node_name.equals("ModeGLI")) {
+	String mode_level_str = XMLTools.getValue(node);
+	try {
+	  int mode_level = Integer.parseInt(mode_level_str);
+	  setModeLevel(mode_level);
+	}
+	catch(Exception exception) {
+	  DebugStream.println("Exception in Argument.parseXML() - Unexpected but non-fatal");
+	  DebugStream.printStackTrace(exception);
+	}
+      }
+
+    } // for each option
+
+  } // parseXML
+
+  public class ArgumentOption 
+    implements Comparable {
+    public String name;
+    public String description;
+    private String text; // cached version
+
+    public ArgumentOption(String name, String desc) {
+      this.name = name;
+      this.description = desc;
+    }
+
+    public int compareTo(Object obj) {
+      return toString().compareTo(obj.toString());
+    }
+
+    public boolean equals(Object obj) {	    
+      return (obj != null && compareTo(obj) == 0);
+    }
+
+    public String toString() {
+      return name + " "+ StaticStrings.MINUS_CHARACTER + " "+ description;
+    }
+
+    public String getToolTip() {
+      return Utility.formatHTMLWidth(name+": "+description, 80);
+    }
+  }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ArgumentConfiguration.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ArgumentConfiguration.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ArgumentConfiguration.java	(revision 31635)
@@ -0,0 +1,253 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.GComboBox;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.ModalDialog;
+import org.greenstone.gatherer.gui.SimpleMenuBar;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.metadata.MetadataTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+/** This class provides us with a dialog box which allows us to edit the arguments of either a Plugin or a Classifier.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ * @see org.greenstone.gatherer.cdm.Classifier
+ * @see org.greenstone.gatherer.cdm.Plugin
+ */
+public class ArgumentConfiguration
+    extends ModalDialog
+    implements ActionListener {
+    /** The data whose arguments we are editing. */
+    private ArgumentContainer data = null;
+    /** Whether we have successfully edited the arguments associated with the ArgumentContainer or if we have failed to enter required arguments and have instead cancelled (which would cause argument additions to roll back). */
+    private boolean success = false;
+    /** A button to cancel this dialog. */
+    private JButton cancel = null;
+    /** A button to accept the changes and close the dialog. */
+    private JButton ok = null;
+    /** A reference to the ourselves so our inner classes can dispose us like a dialog. */
+    private ArgumentConfiguration self = null;
+    /** The central pane where a list of known arguments is displayed. */
+    private JPanel central_pane = null;
+    /** The size used for an argument label. */
+    static final private Dimension LABEL_SIZE = new Dimension(225, 25);
+    /** Size of a list. */
+    static final private Dimension LIST_SIZE = new Dimension(380, 50);
+    /** The size used for the dialog. */
+    static final private Dimension SIZE = new Dimension(800, 425);
+
+    /** Constructor.
+     * @param data The plugin or classifier whose arguments we are configuring, in the form of its supported <strong>ArgumentContainer</strong> interface.
+     * @see org.greenstone.gatherer.Configuration
+     */
+    public ArgumentConfiguration(ArgumentContainer data) {
+	super(Gatherer.g_man);
+	this.data = data;
+	this.self = this;
+
+	// Create
+	setModal(true);
+	setSize(SIZE);
+	setJMenuBar(new SimpleMenuBar("designingacollection")); // can we tell whether we are doing a classifier or plugin, to make the help more specific??
+	setTitle(Dictionary.get("CDM.ArgumentConfiguration.Title"));
+
+	central_pane = new JPanel();
+	JPanel content_pane = (JPanel) getContentPane();
+
+	JLabel header = new JLabel(Dictionary.get("CDM.ArgumentConfiguration.Header", data.getName()));
+	header.setHorizontalAlignment(JLabel.CENTER);
+	header.setOpaque(true);
+	
+	JPanel button_pane = new JPanel();
+	cancel = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Pure_Cancel_Tooltip"));
+		
+	ok = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+	
+	// Listeners
+	cancel.addActionListener(this);
+	ok.addActionListener(this);
+
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	button_pane.setLayout(new GridLayout(1,2));
+	button_pane.add(ok);
+	button_pane.add(cancel);
+
+	central_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	central_pane.setLayout(new BoxLayout(central_pane, BoxLayout.Y_AXIS));
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(header, BorderLayout.NORTH);
+	JScrollPane scrollPane = new JScrollPane(central_pane);
+	scrollPane.getVerticalScrollBar().setUnitIncrement(16);
+	content_pane.add(scrollPane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	// Now generate a set of controls for each of the arguments.
+	generateControls();
+
+	// Display on screen.
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	screen_size = null;
+    }
+
+    /** Any implementation of ActionListener must include this method so that we can be informed when an action has occured on one of the controls we are listening to.
+     * @param event An <strong>ActionEvent</strong> containing pertinant information about the event that fired this call.
+     * @see org.greenstone.gatherer.cdm.ArgumentConfiguration.ArgumentControl
+     * @see org.greenstone.gatherer.cdm.ArgumentContainer
+     */
+    public void actionPerformed(ActionEvent event) {
+	boolean cont = true;
+	if (event.getSource() == ok) {
+	    // Clear the current focus to ensure components such as combobox have correctly updated
+	    // Loop through each of the controls in the central pane, updating the matching argument as necessary.
+	    for(int i = 0; i < central_pane.getComponentCount(); i++) {
+		Component component = central_pane.getComponent(i);
+		if(component instanceof ArgumentControl) {
+		   cont = cont && ((ArgumentControl)component).updateArgument();
+		}
+	    }
+	    if(cont) {
+		success = true;
+	    }
+	}
+	if(cont) {
+	    dispose();
+	}
+    }
+
+
+    public void addOKButtonActionListener(ActionListener action_listener)
+    {
+	ok.addActionListener(action_listener);
+    }
+
+
+    /** Destructor. */
+    public void destroy() {
+	cancel = null;
+	central_pane = null;
+	//custom_pane = null;
+	//custom = null;
+	data = null;
+	ok = null;
+	self = null;
+    }
+
+    /** Method which actually forces the dialog to be shown on screen.
+     * @return <i>true</i> if the user completed configuration and pressed ok, <i>false</i> otherwise.
+     */
+    public boolean display() {
+	setVisible(true);
+	return success;
+    }
+
+    private void addHeader(String name, Color color) {
+	JPanel header = new JPanel();
+	header.setBackground(color);
+	JPanel inner_pane = new JPanel();
+	inner_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(5,5,5,5), BorderFactory.createRaisedBevelBorder()));
+	inner_pane.setBackground(color);
+	JLabel header_label = new JLabel("<html><strong>" + name + "</strong></html>");
+	header_label.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+	header_label.setHorizontalAlignment(JLabel.CENTER);
+	header_label.setOpaque(true);
+
+	// Layout
+	inner_pane.setLayout(new BorderLayout());
+	inner_pane.add(header_label, BorderLayout.CENTER);
+
+	header.setLayout(new BorderLayout());
+	header.add(inner_pane, BorderLayout.CENTER);
+	central_pane.add(header);
+    }
+
+    /** Method to iterate through the arguments associated with whatever argument container we are building an argument control view for, creating the appropriate controls for each.
+     * @see org.greenstone.gatherer.cdm.Argument
+     * @see org.greenstone.gatherer.cdm.ArgumentConfiguration.ArgumentControl
+     */
+    private void generateControls() {
+	// Alternating colours to show inheritance
+	Color colour_one = Configuration.getColor("coloring.collection_heading_background", false);
+	Color colour_two = Configuration.getColor("coloring.collection_tree_background", false);
+	boolean coloured = false;
+	ArrayList arguments = data.getArguments();
+	int total_height = 250;
+	// not currently used
+	//int current_mode = Configuration.getMode();
+
+	String previous_owner = "";
+	if(arguments.size() > 0) {
+	    previous_owner = ((Argument) arguments.get(0)).getOwner();
+	    addHeader(previous_owner, colour_two);
+	}
+
+	for(int i = 0; i < arguments.size(); i++) {
+	    Argument argument = (Argument) arguments.get(i);
+	    String owner = argument.getOwner();
+	    if(previous_owner != argument.getOwner()) {
+	    ///ystem.err.println("previous owner is different from current owner");
+		coloured = !coloured;
+		previous_owner = argument.getOwner();
+		addHeader(previous_owner, (coloured ? colour_one : colour_two));
+	    }
+	    if(!argument.isHiddenGLI()) {
+		ArgumentControl argument_control = new ArgumentControl(argument, false, null);
+		total_height = total_height - argument_control.getPreferredSize().height;
+		// Create
+		if(coloured) {
+		    argument_control.setBackground(colour_one);
+		}
+		else {
+		    argument_control.setBackground(colour_two);
+		}
+
+		central_pane.add(argument_control);
+	    }
+	}
+	if(total_height > 0) {
+	    JPanel filler = new JPanel();
+	    filler.setPreferredSize(new Dimension(100, total_height));
+	    filler.setSize(new Dimension(100, total_height));
+	    central_pane.add(filler);
+	}
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ArgumentContainer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ArgumentContainer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ArgumentContainer.java	(revision 31635)
@@ -0,0 +1,289 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.*;
+
+
+/** This class provides an interface to any class that contains a list of Arguments.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+abstract public class ArgumentContainer 
+extends ArrayList 
+implements Comparable, DOMProxyListEntry, Serializable  {
+
+	// The DOM Element this container is modelled on
+	protected Element element;
+	protected boolean is_abstract = false;
+	protected ArgumentContainer super_container;
+	protected String description;
+	protected String name;
+
+	/** Constructor used in DOMProxyListModel initializations
+	 */
+	public ArgumentContainer() {
+	}
+
+	public ArgumentContainer(Element element, ArgumentContainer base_container) {
+
+		super();
+		this.element = element;
+		this.name = element.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+
+		// Parse in any argument options for this container, keeping a list of the ones found
+		HashMap known_arguments = new HashMap();
+		NodeList option_elements = element.getElementsByTagName(StaticStrings.OPTION_ELEMENT);
+		int option_elements_length = option_elements.getLength();
+		for(int i = 0; i < option_elements_length; i++) {
+			Element option_element = (Element) option_elements.item(i);
+			Argument argument = new Argument(option_element);
+			///ystem.err.println("Rebuilding existing argument: " + argument.getName());
+			known_arguments.put(argument.getName(), argument);
+		}
+		// If a base classifier was given
+		if(base_container != null) {
+			// Copy the details, and add a reference to whatever the base container's super container is
+			description = base_container.getDescription();
+			// Now search through the 'dummy' arguments belonging to the base container. For each found, if it is already assigned, fill out further details such as type. If any are found that are not already assigned for this container, copy them and add them, but without a value.
+			ArrayList all_arguments = base_container.getArguments();
+			int argument_count = all_arguments.size();
+			for(int j = 0; j < argument_count; j++) {
+				Argument base_argument = (Argument) all_arguments.get(j);
+				String base_argument_name = base_argument.getName();
+				///ystem.err.println("Library indicates this container should have an argument: " + base_argument_name);
+				Argument existing_argument = (Argument) known_arguments.get(base_argument_name);
+				// Found an existing argument. Complete its details
+				if(existing_argument != null) {
+					///ystem.err.println("Found existing argument. Filling out details.");
+					known_arguments.remove(base_argument_name);
+					existing_argument.setDisplayName(base_argument.getDisplayName());
+					existing_argument.setDefaultValue(base_argument.getDefaultValue());
+					existing_argument.setDescription(base_argument.getDescription());
+					existing_argument.setOptions(base_argument.getOptions());
+					existing_argument.setRequired(base_argument.isRequired());
+					existing_argument.setType(base_argument.getType());
+					existing_argument.setMinimum(base_argument.getMinimum());
+					existing_argument.setMaximum(base_argument.getMaximum());
+					existing_argument.setOwner(base_argument.getOwner());
+					existing_argument.setHiddenGLI(base_argument.isHiddenGLI());
+					add(existing_argument);
+				}
+				// No existing argument. Copy base_argument and add it, but set its assigned flag to false 
+				else {
+					///atherer.println("No such argument. Adding new, unassigned, argument.");
+					// The trick thing is that we have to create a new element in the DOM as well.
+					Argument new_argument = base_argument.copy();
+					Element argument_element = CollectionConfiguration.createElement(StaticStrings.OPTION_ELEMENT);
+					argument_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, base_argument_name);
+					argument_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+					new_argument.setElement(argument_element);
+					// All done. Add it.
+					element.appendChild(argument_element);
+					add(new_argument);
+				}
+			}
+		}
+
+		// The first time we do this, the args come from the config file and
+		// may contain invalid args. Here we remove them from the DOM.
+		if (!known_arguments.isEmpty()) {
+			Set entry_set = known_arguments.entrySet();
+			Iterator entry_iterator = entry_set.iterator();
+			while (entry_iterator.hasNext()) {
+				Argument arg = (Argument)((Map.Entry)entry_iterator.next()).getValue();
+				Element e = arg.getElement();
+				element.removeChild(e);
+			}
+		}
+	}
+
+
+
+
+	/** Constructor used for the plugins that are in the DOMProxyList */
+	/** Method to add an argument to this container. Only adds the argument if it isn't already present.
+	 * @param argument The <strong>Argument</strong> to add.
+	 */
+	public void addArgument(Argument argument) {
+		if(element == null && !contains(argument)) {
+			add(argument);
+			argument.setOwner(name);
+		}
+	}
+
+	/** Method to compare two containers for ordering.
+	 * @param object The container we are comparing to, as an <strong>Object</strong>.
+	 * @return An <i>int</i> specifying the container order, using values as set out in String.
+	 * @see java.lang.String#compareTo
+	 */
+	public int compareTo(Object object) {
+		if(object == null ) {
+			return -1;
+		}
+		return toString().compareTo(object.toString());
+	}
+
+	/** Method to determine if two containers are equal.
+	 * @param object The container to test against, as an <strong>Object</strong>.
+	 * @return <i>true</i> if they match (based on the equals method), <i>false</i> otherwise.
+	 */
+
+	public boolean equals(Object object) {
+		return (compareTo(object) == 0);
+	}
+
+	abstract public DOMProxyListEntry create(Element element);
+	/** Method to retrieve an argument by its name.
+	 * @param name The name of the argument as a <strong>String</strong>.
+	 * @return The <strong>Argument</strong> requested, or <i>null</i> if no such argument.
+	 */
+	public Argument getArgument(String name) {
+		// The name given may still include the '-'
+		if(name.startsWith("-")) {
+			name = name.substring(1);
+		}
+		//ArrayList arguments = getArguments(true, true);
+		ArrayList arguments = getArguments();
+		for(int i = 0; i < arguments.size(); i++) {
+			Argument argument = (Argument)arguments.get(i);
+			if(argument.getName().equals(name)) {
+				return argument;
+			}
+		}
+		return null;
+	}
+
+	/** Method to retrieve the list of arguments from this container. Note that this method returns both the containers arguments plus its 'supers' arguments if any, and alphabetically orders them.
+	 * @return the arguments within a ArrayList
+	 */
+	public ArrayList getArguments() {
+		ArrayList arguments = new ArrayList();
+		arguments.addAll(this);
+		if(super_container != null) {
+			ArrayList remainder = super_container.getArguments();
+			remainder.removeAll(arguments);
+			arguments.addAll(remainder);
+		}
+		return arguments;
+
+	}
+
+	public String getDescription() {
+		return description;
+	}
+
+	public Element getElement() {
+		return element;
+	}
+	/** Method to retrieve the name associated with this argument container.
+	 * @return the name as a String
+	 */
+	public String getName() {
+		if(name == null && element != null) {
+			name = element.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+		}
+		return name;
+	}
+
+	public boolean isAbstract() {
+		return is_abstract;
+	}
+
+	public boolean isAssigned() {
+		return (element != null && !element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR));
+	}
+
+	public void setAssigned(boolean assigned) {
+		if(element != null) {
+			element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+		}
+	}
+
+	public void setDescription(String description) {
+		this.description = description;
+	}
+
+	public void setElement(Element element) {
+		this.element = element;
+	}
+
+	public void setIsAbstract(boolean is_abstract) {
+		this.is_abstract = is_abstract;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	/** Method to set the value of the super_container.
+	 * @param super_container The new value of super_container as a <strong>ArgumentContainer</strong>, or <i>null</i> if this class has no inheritance.
+	 */
+	public void setSuper(ArgumentContainer super_container) {
+		this.super_container = super_container;
+	}
+
+	public String toString()
+	{
+		if (element == null) {
+			return name;
+		}
+
+		if (name == null) {
+			name = element.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+		}
+
+		StringBuffer text = new StringBuffer(" ");
+		if (!isAssigned()) {
+			text.append("#");
+		}
+		text.append(name);
+
+		ArrayList arguments = getArguments();
+		for (int i = 0; i < arguments.size(); i++) {
+			Argument argument = (Argument) arguments.get(i);
+			if (argument.isAssigned()) {
+				text.append(" ");
+				text.append(argument.toString());
+			}
+		}
+
+		return text.toString();
+	}
+
+	public ArrayList getArguments(boolean include_normal, boolean include_custom){
+		return this;
+	}
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ArgumentControl.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ArgumentControl.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ArgumentControl.java	(revision 31635)
@@ -0,0 +1,605 @@
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Iterator;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.GComboBox;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSet;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.metadata.MetadataTools;
+
+/** This class encapsulates all the technical difficulty of creating a specific control based on an Argument. */
+public class ArgumentControl
+  extends JPanel {
+
+  static protected Dimension LABEL_SIZE = new Dimension(175, 25);
+  static protected Dimension ROW_SIZE = new Dimension(400, 30);
+  /** The Argument this control will be based on. */
+  private Argument argument = null;
+
+  /** A checkbox to allow enabling or diabling of this Argument. */
+  private JCheckBox enabled = null;
+  /** Some form of editor component, such as a JComboBox or JTextField, used to set parameters to an Argument. */
+  private JComponent value_control = null;
+
+  /** Constructor.
+   */
+  public ArgumentControl(Argument argument, boolean is_enabled, String preset_value) {
+    this.setComponentOrientation(Dictionary.getOrientation());
+    this.argument = argument;
+
+    String tip = "<html>" + argument.getName()+": "+argument.getDescription() + "</html>";
+    tip = Utility.formatHTMLWidth(tip, 80);
+
+    setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+    setBorder(BorderFactory.createEmptyBorder(2,0,2,0));
+    setLayout(new BorderLayout());
+    setPreferredSize(ROW_SIZE);
+		
+    // display the name set in the disp option if there is one
+    // otherwise display name option value instead
+    String dispName = argument.getDisplayName();
+    if(dispName.equals("")){
+      dispName = argument.getName();
+    }
+
+    if (argument.isRequired()) {		
+      //JLabel label = new JLabel(argument.getName());
+      JLabel label = new JLabel(dispName);
+      label.setComponentOrientation(Dictionary.getOrientation());
+      label.setOpaque(false);
+      label.setPreferredSize(LABEL_SIZE);
+      label.setToolTipText(tip);
+      add(label, BorderLayout.LINE_START);
+    } else {
+      //enabled = new JCheckBox(argument.getName());
+      enabled = new JCheckBox(dispName);
+      enabled.setComponentOrientation(Dictionary.getOrientation());
+      enabled.setOpaque(false);
+      enabled.setPreferredSize(LABEL_SIZE);
+      enabled.setToolTipText(tip);
+      add(enabled, BorderLayout.LINE_START);
+    }
+
+    String initial_value;
+    if (preset_value != null && !preset_value.equals("")) {
+      initial_value = preset_value;
+    }
+    else {
+      initial_value = argument.getValue();
+    }
+    if (initial_value == null || initial_value.equals("")) {
+      initial_value = argument.getDefaultValue();
+    }
+    if (initial_value == null) {
+      initial_value = "";
+    }
+
+    switch(argument.getType()) {
+    case Argument.ENUM_STRING:
+    case Argument.ENUM:
+      ArrayList option_list = argument.getOptions();
+      boolean editable = false;
+      if (argument.getType() == Argument.ENUM_STRING) {
+	editable = true;
+      }
+      value_control = new GComboBox(option_list.toArray(), editable, false);
+      value_control.setComponentOrientation(Dictionary.getOrientation());
+			
+      boolean found = selectValue((JComboBox)value_control, initial_value); // also sets the tooltip
+      if (!found && argument.getType() == Argument.ENUM_STRING) {
+	// Its a custom item
+	((JComboBox) value_control).addItem(initial_value);
+	((JComboBox) value_control).setSelectedItem(initial_value);
+      }
+
+      ((JComboBox)value_control).addActionListener(new ToolTipUpdater());
+      break;
+
+    case Argument.FLAG:
+      // Only need the check box.
+      break;
+
+    case Argument.INTEGER:
+      // Build a spinner
+      int initial_int=0;
+      // If there was an original value, set it.
+      try {
+	initial_int = Integer.parseInt(initial_value);
+      } catch (Exception error) {
+	DebugStream.println("ArgumentControl Error: "+error);
+      }
+      if (initial_int < argument.getMinimum()) {
+	initial_int = argument.getMinimum();
+      } else if (initial_int > argument.getMaximum()) {
+	initial_int = argument.getMaximum();
+      }
+
+      JSpinner spinner = new JSpinner(new SpinnerNumberModel(initial_int, argument.getMinimum(), argument.getMaximum(), 1));
+      spinner.setComponentOrientation(Dictionary.getOrientation());
+      // And remember it
+      value_control = spinner;
+      break;
+
+    case Argument.REGEXP:
+    case Argument.STRING:
+      value_control = new JTextField(initial_value);
+      value_control.setComponentOrientation(Dictionary.getOrientation());
+      break;
+
+    case Argument.LANGUAGE:
+      value_control = new GComboBox(CollectionDesignManager.language_manager.getLanguageCodes().toArray(), false);
+      value_control.setComponentOrientation(Dictionary.getOrientation());
+      // we want to display the language name not the code
+      ((JComboBox)value_control).setRenderer(new LanguageListCellRenderer());
+      // Now ensure we have the existing value or default value selected if either exist in our known languages
+      String lang_name = CollectionDesignManager.language_manager.getLanguageName(initial_value);
+      if (lang_name != null) {
+	((JComboBox)value_control).setSelectedItem(initial_value);
+      }
+      break;
+
+    case Argument.METADATA:
+      value_control = new GComboBox(MetadataSetManager.getEveryMetadataSetElement(), false);
+      value_control.setComponentOrientation(Dictionary.getOrientation());
+      // Editable for advanced modes (allows things like dc.Title,ex.Title)
+      if (Configuration.getMode() > Configuration.ASSISTANT_MODE) {
+	((JComboBox) value_control).setEditable(true);
+      }
+      // Now ensure we have the existing value or default value selected if either exist.
+      String existing_value = preset_value;
+      if (existing_value == null || existing_value.equals("")) {
+	existing_value = argument.getValue();
+      }
+      if (existing_value == null || existing_value.equals("")) {
+	// try default value
+	String default_value = argument.getDefaultValue();
+	if (default_value != null) {
+	  // if no namespace for default value, add ex.
+	  // won't work if we want to set a non-metadata value
+	  if (MetadataTools.getMetadataSetNamespace(default_value).equals("")) {
+	    default_value = StaticStrings.EXTRACTED_NAMESPACE+default_value;
+	  }
+	  existing_value = default_value;
+	}
+      }
+	
+      if (existing_value != null && !existing_value.equals("")) {
+	
+	found = selectValue((JComboBox) value_control, existing_value);
+	// It's possible that this is a custom value and so doesn't exist in the combobox
+	if (!found) {
+	  // If so, add it then select it
+	  ((JComboBox) value_control).addItem(existing_value);
+	  ((JComboBox) value_control).setSelectedItem(existing_value);
+	}
+      }
+      break;
+
+    case Argument.METADATA_SET_NAMESPACE:
+      value_control = new JComboBox();
+      value_control.setComponentOrientation(Dictionary.getOrientation());
+      // !! Hack for exploding metadata databases: add the (empty) exploded metadata set
+      File exploded_metadata_set_file = new File(Gatherer.getGLIMetadataDirectoryPath() + "exp" + StaticStrings.METADATA_SET_EXTENSION);
+      MetadataSet exploded_metadata_set = new MetadataSet(exploded_metadata_set_file);
+      Gatherer.c_man.importMetadataSet(exploded_metadata_set);
+
+      // All the loaded metadata sets except the extracted metadata set are applicable
+      ArrayList metadata_sets = MetadataSetManager.getMetadataSets();
+      for (int i = metadata_sets.size() - 1; i >= 0; i--) {
+	MetadataSet metadata_set = (MetadataSet) metadata_sets.get(i);
+	if (!(metadata_set.getNamespace().equals(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE))) {
+	  ((JComboBox)value_control).addItem(metadata_set);
+	}
+      }
+
+      selectValue((JComboBox) value_control, initial_value);
+
+    } // end of switch
+
+    // Enable or disable as necessary.
+    if(argument.isRequired() || argument.isAssigned() || is_enabled) {
+      if (enabled != null) {
+	enabled.setSelected(true);
+      }
+      if(value_control != null) {
+	value_control.setOpaque(true);
+	value_control.setBackground(Color.white);
+	value_control.setEnabled(true);
+	if(value_control instanceof JSpinner) {
+	  // Set enabled
+	  JComponent c = ((JSpinner)value_control).getEditor();
+	  if ( c instanceof JSpinner.DefaultEditor ) {
+	    JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) c;
+	    JFormattedTextField field = editor.getTextField();
+	    field.setEditable(true);
+	    field.setBackground(Color.white);
+	  }
+	}
+      }
+    }
+    else {
+      if (enabled != null) {
+	enabled.setSelected(false);
+      }
+      if(value_control != null) {
+	value_control.setOpaque(true);
+	value_control.setBackground(Color.lightGray);
+	value_control.setEnabled(false);
+	if(value_control instanceof JSpinner) {
+	  // Set enabled
+	  JComponent c = ((JSpinner)value_control).getEditor();
+	  if ( c instanceof JSpinner.DefaultEditor ) {
+	    JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) c;
+	    JFormattedTextField field = editor.getTextField();
+	    field.setEditable(false);
+	    field.setBackground(Color.lightGray);
+	  }
+	}
+      }
+    }
+
+    // Listener
+    if(value_control != null) {
+      if (argument.getType() != Argument.ENUM && argument.getType() != Argument.ENUM_STRING) {
+	// enums have already set tooltips based on option value
+	value_control.setToolTipText(tip);
+      }
+      add(value_control, BorderLayout.CENTER);
+      if (!argument.isRequired()) {
+	enabled.addActionListener(new EnabledListener(value_control));
+      }
+    }
+  }
+
+  public Argument getArgument() {
+    return argument;
+  }
+
+  public String getArgumentName() {
+    return argument.getName();
+  }
+
+  public String getValue() {
+    if(value_control == null) {
+      return null;
+    }
+    if (value_control instanceof JSpinner) {
+      return ((JSpinner)value_control).getValue().toString();
+    }
+    if(value_control instanceof JComboBox) {
+      Object selected_item = ((JComboBox)value_control).getSelectedItem();
+      if (selected_item != null) {
+	if (argument.getType() == Argument.METADATA_SET_NAMESPACE) {
+	  return ((MetadataSet) selected_item).getNamespace();
+	}
+	if (selected_item instanceof Argument.ArgumentOption) {
+	  return ((Argument.ArgumentOption)selected_item).name;
+	}
+	if (selected_item instanceof MetadataElement) {
+	  return ((MetadataElement) selected_item).getFullName();
+	}
+	return selected_item.toString();
+      }    
+      return null;
+    }
+    if(value_control instanceof JTextField) {
+      return ((JTextField)value_control).getText();
+    }
+    return null;
+  }
+  /** Retrieve the control used for storing values.
+   * @return JComponent
+   */
+  public JComponent getValueControl() {
+    return value_control;
+  }
+
+  public boolean isEnabled() {
+    if (enabled == null) {
+      return true; // always enabled
+    } 
+    return enabled.isSelected();
+  }
+
+
+  /** Identifies this control by returning the name of the Argument it is based on.
+   * @return The name of the Argument as a <strong>String</strong>.
+   * @see org.greenstone.gatherer.cdm.Argument
+   */
+  public String toString() {
+    return argument.getName();
+  }
+  /** Updates the enwrapped Argument using the values provided by the controls.
+   * @return <i>true</i> if the update was successful, <i>false</i> otherwise.
+   * @see org.greenstone.gatherer.cdm.ArgumentConfiguration.Argument.ArgumentOption
+   * @see org.greenstone.gatherer.cdm.Language
+   */
+  public boolean updateArgument() {
+    if(argument.isRequired() || enabled.isSelected() ) {
+      argument.setAssigned(false);
+      String result = null;
+      switch(argument.getType()) {
+      case Argument.ENUM:
+	Argument.ArgumentOption option = (Argument.ArgumentOption)((JComboBox)value_control).getSelectedItem();
+	// its impossible not to choose an entry
+	argument.setValue(option.name);
+	argument.setAssigned(true);
+	return true;
+      case Argument.ENUM_STRING:
+	Object new_value_raw = ((JComboBox)value_control).getSelectedItem();
+	if (new_value_raw instanceof Argument.ArgumentOption) {
+	  argument.setValue(((Argument.ArgumentOption)new_value_raw).name);
+	} else {
+	  // have entered a new string
+	  String new_value = new_value_raw.toString();
+	  if (new_value.length() > 0) {
+	    argument.setValue(new_value);
+	  }
+	  else {
+	    String args[] = new String[1];
+	    args[0] = argument.getName();
+	    if(argument.isRequired()) {
+	      JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.Required_Argument", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
+	    }
+	    // They've left the field blank
+	    else {
+	      JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.No_Value", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
+	      argument.setValue(null);
+	    }
+	    args = null;
+	    return false;
+	  }
+	}
+	argument.setAssigned(true);
+	return true;
+      case Argument.FLAG:
+	// Should have already been handled above.
+	argument.setAssigned(true);
+	return true;
+      case Argument.INTEGER:
+	result = ((JSpinner)value_control).getValue().toString();
+	if(result.length() > 0) {
+	  // Test if the value entered is a valid int.
+	  try {
+	    int x = Integer.parseInt(result);
+	  }
+	  catch(NumberFormatException nfe) {
+	    String args[] = new String[2];
+	    args[0] = argument.getName();
+	    args[1] = result;
+	    JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.Bad_Integer", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
+	    args = null;
+	    return false;
+	  }
+	  argument.setValue(result);
+	}
+	else {
+	  String args[] = new String[1];
+	  args[0] = argument.getName();
+	  if(argument.isRequired()) {
+	    JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.Required_Argument", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
+	  }
+	  // They've left the field blank
+	  else {
+	    JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.No_Value", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
+	    argument.setValue(null);
+	  }
+	  args = null;
+	  return false;
+	}
+	argument.setAssigned(true);
+	return true;
+      case Argument.LANGUAGE:
+	String language = (((JComboBox)value_control).getSelectedItem()).toString();
+	argument.setValue(language);
+	// Kinda lucked out here. Its impossible not to choose an entry from these comboboxes as they are restricted.
+	argument.setAssigned(true);
+	return true;
+      case Argument.METADATA:
+	  String meta_value = (((JComboBox)value_control).getSelectedItem()).toString();
+	  if(meta_value.length() > 0) {
+	      
+	      argument.setValue(meta_value);
+	  }
+	  else {
+	    String args[] = new String[1];
+	    args[0] = argument.getName();
+	    if(argument.isRequired()) {
+	      JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.Required_Argument", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
+	    }
+	    // They've left the field blank
+	    else {
+	      JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.No_Value", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
+	      argument.setValue(null);
+	    }
+	    args = null;
+	    return false;
+	  }
+      
+	argument.setAssigned(true);
+	return true;
+      case Argument.REGEXP:
+      case Argument.STRING:
+	result = ((JTextField)value_control).getText();
+	if(result.length() > 0) {
+	  argument.setValue(result);
+	}
+	else {
+	  String args[] = new String[1];
+	  args[0] = argument.getName();
+	  if(argument.isRequired()) {
+	    JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.Required_Argument", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
+	  }
+	  // They've left the field blank
+	  else {
+	    JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.No_Value", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
+	    argument.setValue(null);
+	  }
+	  args = null;
+	  return false;
+	}
+	argument.setAssigned(true);
+	return true;
+      }
+      return false;
+    }
+    else {
+      argument.setAssigned(false);
+      return true;
+    }
+  }
+
+
+  public boolean updateArgument(boolean checkRequired) {
+
+
+    if (checkRequired){
+      return updateArgument();
+    }
+    else{
+      if (argument.getType() == Argument.STRING){
+	String result = ((JTextField)value_control).getText();
+
+	if(result.length() > 0) {
+	  argument.setValue(result);
+	  argument.setAssigned(true);                    
+	}
+      }	
+    }
+
+    return true;
+
+  }
+
+
+
+  /** Method to ensure that a certain value is selected, if it exists within that combobox to begin with.
+   * @param combobox The <strong>JComboBox</strong> whose selection we are trying to preset.
+   * @param target The desired value of the selection as a <strong>String</strong>.
+   * @return true if the item was found and selected, false otherwise
+   * @see org.greenstone.gatherer.cdm.ArgumentConfiguration.Argument.ArgumentOption
+   */
+  public static boolean selectValue(JComboBox combobox, String target)
+  {
+    for (int i = 0; i < combobox.getItemCount(); i++) {
+      Object object = combobox.getItemAt(i);
+
+      if (object instanceof Argument.ArgumentOption) {
+	Argument.ArgumentOption opt = (Argument.ArgumentOption) object;
+	if (opt.name.startsWith(target)) {
+	  combobox.setSelectedIndex(i);
+	  combobox.setToolTipText(opt.getToolTip());
+	  return true;
+	}
+      }
+      else if (object instanceof MetadataElement) {
+	if(((MetadataElement)object).getFullName().equals(target)) {
+	  combobox.setSelectedIndex(i);
+	  return true;
+	}
+      }
+      else if (object.toString().equals(target)) {
+	combobox.setSelectedIndex(i);
+	return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  /** Forces the control into an 'enabled' mode. */
+  public void setEnabled() {
+    enabled.setSelected(true);
+  }
+  /** Explicitly sets the value of a JTextField type control to the given String.
+   * @param value_str The new value of the control as a <strong>String</strong>.
+   */
+  public void setValue(String value_str) {
+    ((JTextField)value_control).setText(value_str);
+  }
+
+  /** Listens for actions apon the enable checkbox, and if detected enables or diables control appropriately. */
+  private class EnabledListener
+    implements ActionListener {
+    /** An editor component, such as a JComboBox or JTextField, that might have its enabled state changed by this listener. */
+    private JComponent target = null;
+
+    /** Constructor. */
+    public EnabledListener(JComponent target) {
+      this.target = target;
+    }
+
+    /** Any implementation of ActionListener must include this method so that we can be informed when an action has been performed on or registered check box, prompting us to change the state of the other controls as per the users request.
+     * @param event An <strong>ActionEvent</strong> containing information about the click.
+     */
+    public void actionPerformed(ActionEvent event) {
+      if (this.target == null) {
+	return;
+      }
+      JCheckBox source = (JCheckBox)event.getSource();
+      if(source.isSelected()) {
+	target.setBackground(Color.white);
+	target.setEnabled(true);
+      }
+      else {
+	target.setBackground(Color.lightGray);
+	target.setEnabled(false);
+      }
+      // Special case of stupid JSpinners who don't let their backgrounds change properly.
+      if(target instanceof JSpinner) {
+	JSpinner spinner = (JSpinner) target;
+	JComponent c = spinner.getEditor();
+	if ( c instanceof JSpinner.DefaultEditor ) {
+	  JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) c;
+	  JFormattedTextField field = editor.getTextField();
+	  field.setEditable(source.isSelected());
+	  if(source.isSelected()) {
+	    field.setBackground(Color.white);
+	  }
+	  else {
+	    field.setBackground(Color.lightGray);
+	  }
+	}
+      }
+    }
+  }
+
+
+  /** Listener that sets the tooltip associated to a combobox to the tooltip relevant to the selected item. */
+  private class ToolTipUpdater
+    implements ActionListener {
+    /** Any implementation of an ActionListener must include this method so that we can be informed when the selection in a combobox has changed and update the tooltip accordingly.
+     * @param event An <strong>ActionEvent</strong> containing information about the action that fired this call.
+     */
+    public void actionPerformed(ActionEvent event) {
+      JComboBox source = (JComboBox)event.getSource();
+      Object object = source.getSelectedItem();
+      if(object instanceof Argument.ArgumentOption) {
+	Argument.ArgumentOption opt = (Argument.ArgumentOption)object;
+	if(opt != null) {
+	  source.setToolTipText(opt.getToolTip());
+	}
+	else {
+	  source.setToolTipText(StaticStrings.EMPTY_STR);
+	}
+      }
+    }
+  }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/BuildTypeManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/BuildTypeManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/BuildTypeManager.java	(revision 31635)
@@ -0,0 +1,292 @@
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.EventListener;
+import javax.swing.event.EventListenerList;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.ModalDialog;
+
+public class BuildTypeManager {
+
+    /** The size of this new collection dialog box. */
+    static private Dimension DIALOG_SIZE = new Dimension(600, 280);
+
+    static final public String BUILD_TYPE_MG = "mg";
+    static final public String BUILD_TYPE_MGPP = "mgpp";
+    static final public String BUILD_TYPE_LUCENE = "lucene";
+
+    static final public String BUILD_TYPE_MG_STR = "MG";
+    static final public String BUILD_TYPE_MGPP_STR = "MGPP";
+    static final public String BUILD_TYPE_LUCENE_STR = "Lucene";
+    
+    static final public String[] BUILD_TYPES = {  BUILD_TYPE_MGPP, BUILD_TYPE_MG, BUILD_TYPE_LUCENE };
+
+    private EventListenerList listeners = null;
+    /** the buildtype element in the config file - uses CollectionMeta */
+    public CollectionMeta build_type_meta = null;
+    private Control controls = null;
+
+    protected BuildTypeManager manager = null;
+    public BuildTypeManager() {
+	build_type_meta = new CollectionMeta(CollectionDesignManager.collect_config.getBuildType());
+	if (getBuildType().equals("")) {
+	    build_type_meta.setValue(BUILD_TYPE_MG);
+	    // must have an old collection, assume MG
+	}
+	listeners = new EventListenerList();
+	manager = this;
+    }
+
+    public void addBuildTypeListener(BuildTypeListener listener) {
+	listeners.add(BuildTypeListener.class, listener);
+    }
+    
+    protected void notifyListeners(String new_build_type) {
+	Object[] concerned = listeners.getListenerList();
+	for(int i = 0; i < concerned.length ; i++) {
+	    if(concerned[i] == BuildTypeListener.class) {
+		((BuildTypeListener)concerned[i+1]).buildTypeChanged(new_build_type);
+	    }
+	}
+	concerned = null;
+    }
+    
+	
+    public void promptForNewBuildType() {
+
+	BuildTypePrompt btp = new BuildTypePrompt(build_type_meta.getValue(CollectionMeta.TEXT));	
+    }
+
+    public boolean isMGPP () {
+
+	return getBuildType().equals(BUILD_TYPE_MGPP);
+    }
+    public boolean isMG () {
+
+	return getBuildType().equals(BUILD_TYPE_MG);
+    }
+    public boolean isLucene () {
+
+	return getBuildType().equals(BUILD_TYPE_LUCENE);
+    }
+
+    
+    public String getBuildType() {
+	return build_type_meta.getValue(CollectionMeta.TEXT);
+    }
+
+    public Control getControls() {
+	if (controls == null) {
+	    controls = new BuildTypeControl();
+	}
+	return controls;
+    }
+
+    public interface BuildTypeListener
+	extends EventListener {
+	public void buildTypeChanged(String new_build_type);
+    }
+
+    private class BuildTypeControl 
+	extends JPanel
+	implements Control, BuildTypeListener {
+
+	JLabel label = null;
+	JButton change_button = null;
+	
+	public BuildTypeControl() {
+	    super();
+	    this.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel spacer_panel = new JPanel();
+	    spacer_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel main_panel = new JPanel();
+            main_panel.setComponentOrientation(Dictionary.getOrientation());
+	    /* may be CDM.BuildTypeManager.mg, CDM.BuildTYpeManager.mgpp, CDM.BuildTypeManager.lucene */
+	    label = new JLabel(Dictionary.get("CDM.BuildTypeManager.Current_Type", getBuildTypeString(getBuildType())));
+            label.setComponentOrientation(Dictionary.getOrientation());
+	    
+            change_button = new GLIButton(Dictionary.get("CDM.BuildTypeManager.Change"), Dictionary.get("CDM.BuildTypeManager.Change_Tooltip"));
+	    	    
+	    change_button.addActionListener(new ActionListener() {
+		    public void actionPerformed(ActionEvent event) {
+			promptForNewBuildType();
+		    }
+		});
+
+	    main_panel.setLayout(new BorderLayout(10,10));
+	    main_panel.add(label, BorderLayout.CENTER);
+	    main_panel.add(change_button, BorderLayout.LINE_END);
+
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(spacer_panel, BorderLayout.CENTER);
+	    add(main_panel, BorderLayout.LINE_END);
+
+	    manager.addBuildTypeListener(this);
+	}
+
+	public void loseFocus() {}
+	public void gainFocus() {}
+	public void destroy() {}
+
+	private String getBuildTypeString(String build_type) {
+	    if (build_type.equals(BUILD_TYPE_MG)) {
+		return BUILD_TYPE_MG_STR;
+	    }
+	    if (build_type.equals(BUILD_TYPE_MGPP)) {
+		return BUILD_TYPE_MGPP_STR;
+	    }
+	    if (build_type.equals(BUILD_TYPE_LUCENE)) {
+		return BUILD_TYPE_LUCENE_STR;
+	    }
+	    return "";
+	}
+
+		
+	public void buildTypeChanged(String new_build_type) {
+	    label.setText(Dictionary.get("CDM.BuildTypeManager.Current_Type", getBuildTypeString(new_build_type)));
+	}
+    }
+    
+    private class BuildTypePrompt 
+	extends ModalDialog {
+	
+	private JDialog self;
+	
+	private JRadioButton mg_button = null;
+	private JRadioButton mgpp_button = null;
+	private JRadioButton lucene_button = null;
+
+	private JTextArea description_textarea = null;
+	
+	JButton ok_button = null;
+	JButton cancel_button = null;
+	
+	public BuildTypePrompt(String current_build_type) {
+	    super(Gatherer.g_man, true);
+	    this.self = this;
+	    setSize(DIALOG_SIZE);
+	    setTitle(Dictionary.get("CDM.BuildTypeManager.Title"));
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    mg_button = new JRadioButton(BUILD_TYPE_MG_STR);
+            mg_button.setComponentOrientation(Dictionary.getOrientation());
+	    mg_button.setActionCommand(BUILD_TYPE_MG);
+	    mgpp_button = new JRadioButton(BUILD_TYPE_MGPP_STR);
+            mgpp_button.setComponentOrientation(Dictionary.getOrientation());
+	    mgpp_button.setActionCommand(BUILD_TYPE_MGPP);
+	    lucene_button = new JRadioButton(BUILD_TYPE_LUCENE_STR);
+            lucene_button.setComponentOrientation(Dictionary.getOrientation());
+	    lucene_button.setActionCommand(BUILD_TYPE_LUCENE);
+	    
+	    BuildTypeButtonListener btbl = new BuildTypeButtonListener();
+	    mg_button.addActionListener(btbl);
+	    mgpp_button.addActionListener(btbl);
+	    lucene_button.addActionListener(btbl);
+
+	    ButtonGroup build_type_group = new ButtonGroup();
+	    build_type_group.add(mgpp_button);
+	    build_type_group.add(mg_button);
+	    build_type_group.add(lucene_button);
+            
+	    if (current_build_type != null) {
+		if (current_build_type.equals(BUILD_TYPE_MGPP)) {
+		    mgpp_button.setSelected(true);
+		} else if (current_build_type.equals(BUILD_TYPE_MG)) {
+		    mg_button.setSelected(true);
+		} else if (current_build_type.equals(BUILD_TYPE_LUCENE)) {
+		    lucene_button.setSelected(true);
+		} 
+	    }
+	    
+	    JPanel radio_pane = new JPanel();
+	    radio_pane.setLayout(new GridLayout(3,1));
+	    radio_pane.add(mgpp_button);
+	    radio_pane.add(mg_button);
+	    radio_pane.add(lucene_button);
+            radio_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    description_textarea = new JTextArea();
+	    description_textarea.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    /* may be CDM.BuildTypeManager.mg_Description, CDM.BuildTYpeManager.mgpp_Description, CDM.BuildTypeManager.lucene_Description */
+	    description_textarea.setText(Dictionary.get("CDM.BuildTypeManager."+current_build_type+"_Description"));
+	    description_textarea.setLineWrap(true);
+	    description_textarea.setWrapStyleWord(true);
+	    description_textarea.setComponentOrientation(Dictionary.getOrientation());
+            
+            cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+	    
+	    cancel_button.addActionListener(new ActionListener() {
+		    public void actionPerformed(ActionEvent event) {
+			self.dispose();
+		    }
+		});
+	    
+	    ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+	    
+	    ok_button.addActionListener(new ActionListener() {
+		    public void actionPerformed(ActionEvent event) {
+			String new_build_type = BUILD_TYPE_MGPP;
+			if (mgpp_button.isSelected()) {
+			    new_build_type = BUILD_TYPE_MGPP;
+			} else if (mg_button.isSelected()) {
+			    new_build_type = BUILD_TYPE_MG;
+			} else if (lucene_button.isSelected()) {
+			    new_build_type = BUILD_TYPE_LUCENE;
+			}
+			if (!build_type_meta.getValue(CollectionMeta.TEXT).equals(new_build_type)) {
+			    manager.notifyListeners(new_build_type);
+			    build_type_meta.setValue(new_build_type);
+			}
+			self.dispose();
+		    }
+		});
+	    // tell the CDM that we have (possibly) changed
+	    ok_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    JPanel button_pane = new JPanel();
+	    button_pane.setLayout(new GridLayout(1,2));
+	    button_pane.add(ok_button);
+	    button_pane.add(cancel_button);
+            button_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel content_pane = (JPanel) getContentPane();
+	    content_pane.setOpaque(true);
+	    content_pane.setLayout(new BorderLayout());
+	    content_pane.add(radio_pane, BorderLayout.NORTH);
+	    content_pane.add(new JScrollPane(description_textarea), BorderLayout.CENTER);
+	    content_pane.add(button_pane, BorderLayout.SOUTH);
+            content_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    // Center and display.
+	    Dimension screen_size = Configuration.screen_size;
+	    this.setLocation((screen_size.width - DIALOG_SIZE.width) / 2, (screen_size.height - DIALOG_SIZE.height) / 2);
+	    this.setVisible(true); // blocks until the dialog is killed
+	       
+	}
+
+	public void loseFocus() {
+
+	}
+	public void gainFocus() {
+
+	}
+	public void destroy() {
+
+	}
+	
+	private class BuildTypeButtonListener
+	    implements ActionListener {
+
+	    public void actionPerformed(ActionEvent event) {
+		description_textarea.setText(Dictionary.get("CDM.BuildTypeManager."+event.getActionCommand()+"_Description"));
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Classifier.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Classifier.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Classifier.java	(revision 31635)
@@ -0,0 +1,102 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.greenstone.Classifiers;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.w3c.dom.*;
+
+
+/** This class is responsible for storing information from a parsed classinfo.pl call in such a way that it allows easy access to parsed details for the purposes of user design and specification of classifiers.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class Classifier
+    extends ArgumentContainer
+{
+    static final public String CLASSIFIER_PREFIX = "CL";
+
+    private boolean loading_options_failed = false;
+
+
+    /** Constructor used only in DOMProxyListModel initializations.
+     */
+    public Classifier() {
+    }
+
+    
+    public Classifier(Element element, Classifier base_classifier) {
+	super(element, base_classifier);
+    }
+
+
+    /** The assigned classifier constructor.
+     * @param element the DOM Element this classifier is based upon
+     */
+    public DOMProxyListEntry create(Element element) {
+	String classifier_name = element.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+	// Determine the base classifier from the classifier name
+	Classifier base_classifier = Classifiers.getClassifier(classifier_name, true);
+	Classifier classifier = new Classifier(element, base_classifier);
+	if (base_classifier == null) {
+	    // this is kind of a place holder classifier for one which is invalid. It won't get written out to the config file when its next saved.
+	    classifier.setAssigned(false);
+	}
+	base_classifier = null;
+	classifier_name = null;
+	return classifier;
+    }
+
+
+    public boolean didLoadingOptionsFail()
+    {
+	return loading_options_failed;
+    }
+
+
+    /** Generate the string showing this classifiers position. */
+    public String getPositionString() {
+	String position_string = CLASSIFIER_PREFIX;
+	if(element != null) {
+	    // Determine our place in the collect.cfg file
+	    int position_int = CollectionDesignManager.classifier_manager.indexOf(this) + 1;
+	    if(position_int != -1) {
+		position_string = position_string + position_int;
+	    }
+	}
+	return position_string;
+    }
+
+
+    public void setLoadingOptionsFailed()
+    {
+	this.loading_options_failed = true;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ClassifierManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ClassifierManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ClassifierManager.java	(revision 31635)
@@ -0,0 +1,632 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.jar.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.apache.xerces.parsers.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.greenstone.Classifiers;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.GComboBox;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+import org.xml.sax.*;
+
+/** This class is responsible for keeping track of all the classifiers assigned to this collection, and providing methods for adding and removing them.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class ClassifierManager
+extends DOMProxyListModel {
+
+    private DatabaseTypeManager database_type_manager = null;
+    private String database_type = null;
+
+    /** The controls for editing the contents of this manager. */
+    private Control controls = null;
+    
+    private DOMProxyListModel model;
+    private Classifier default_classifier = null;
+
+    /** Constructor.
+     * @see org.greenstone.gatherer.cdm.DynamicListModel
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     */
+    public ClassifierManager () {
+        super (CollectionDesignManager.collect_config.getDocumentElement (), StaticStrings.CLASSIFY_ELEMENT, new Classifier ());
+        this.model = this;
+        DebugStream.println ("ClassifierManager: " + getSize () + " classifiers parsed.");
+
+	database_type_manager = new DatabaseTypeManager();
+	database_type = database_type_manager.getDatabaseType();
+        
+        // Force the assigned classifiers to be loaded and cached now
+        for (int i = 0; i < getSize (); i++) {
+            getElementAt (i);
+        }
+    }
+    
+    
+    /** Retrieve a list of the classifiers that are available to be added to the collection. */
+    private Object[] getAvailableClassifiers () {
+        ArrayList available = new ArrayList ();
+        
+        // Add all the non-abstract core Greenstone classifiers
+        ArrayList classifiers_list = Classifiers.getClassifiersList ();
+        for (int i = 0; i < classifiers_list.size (); i++) {
+            Classifier classifier = (Classifier) classifiers_list.get (i);
+            if (!classifier.isAbstract ()) {
+                available.add (classifier);
+            }
+	    if (classifier.toString().equals("List")) {
+		default_classifier = classifier;
+	    }
+        }
+        
+        // Sort the available classifiers into alphabetical order
+        Collections.sort (available);
+        
+        return available.toArray ();
+    }
+    
+    
+    /** Method to assign a classifier (i.e., add a new classifier onto the list).
+     * @param classifier The base <strong>Classifier</strong> to assign.
+     * @see org.greenstone.gatherer.cdm.DynamicListModel
+     */
+    private void assignClassifier (Classifier classifier) {
+        if(!contains (classifier)) {
+            Element element = classifier.getElement ();
+            // Locate where we should insert this new classifier.
+            Node target_node = CollectionConfiguration.findInsertionPoint (element);
+            add (root, classifier, target_node);
+
+            // tell the format manager to update the names of its format statements
+            Gatherer.c_man.getCollection ().cdm.format_manager.refresh ();
+        }
+    }
+    
+    
+    /** Destructor. */
+    public void destroy () {
+        if (controls != null) {
+            controls.destroy ();
+            controls = null;
+        }
+    }
+    
+    
+    /** Method to retrieve the classifier with the given index.
+     * @param index The index of the desired classifier as an <i>int</i>.
+     * @return The requested Classifier or <i>null</i> if no such classifier exists.
+     */
+    public Classifier getClassifier (int index) {
+        if(0 <= index && index < getSize ()) {
+            return (Classifier) getElementAt (index);
+        }
+        return null;
+    }
+    
+    /** Method to retrieve the control for this manager.
+     * @return the Control for editing classifiers
+     */
+    public Control getControls () {
+        if(controls == null) {
+            // Build controls
+            this.controls = new ClassifierControl ();
+        }
+        return controls;
+    }
+    
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged (int mode) {
+        
+    }
+    
+    
+    
+    /** Determine if the Phind classifier has been assigned.
+     * @return true if it has, false otherwise
+     */
+    public boolean isPhindClassifierAssigned () {
+        for(int i = 0; i < getSize (); i++) {
+            Classifier classifier = (Classifier) getElementAt (i);
+            if(classifier.getName ().equalsIgnoreCase (StaticStrings.PHIND_CLASSIFIER)) {
+                return true;
+            }
+            classifier = null;
+        }
+        return false;
+    }
+    
+    /** Determine if a DateList classifier has been assigned
+     * @return true if it has, false otherwise
+     */
+    public boolean isDateListClassifierAssigned () {
+        for(int i = 0; i < getSize (); i++) {
+            Classifier classifier = (Classifier) getElementAt (i);
+            if(classifier.getName ().equalsIgnoreCase (StaticStrings.DATELIST_CLASSIFIER)) {
+                return true;
+            }
+            classifier = null;
+        }
+        return false;
+        
+    }
+    /** Method to move a classifier in the list order.
+     * @param classifier the Classifier you want to move.
+     * @param direction true to move the classifier up, false to move it down.
+     * @param all true to move to move all the way, false for a single step.
+     */
+    private void moveClassifier (Classifier classifier, boolean direction, boolean all) {
+        if(getSize () < 2) {
+            DebugStream.println ("Not enough classifiers to allow moving.");
+            return;
+        }
+        if(all) {
+            // Move to top
+            if(direction) {
+                // Remove the moving classifier
+                remove (classifier);
+                // Retrieve the first classifier
+                Classifier first_classifier = (Classifier) getElementAt (0);
+                // Add the moving classifier before the first classifier
+                addBefore (classifier, first_classifier);
+                first_classifier = null;
+            }
+            else {
+                // Remove the moving classifier
+                remove (classifier);
+                // And add after last classifier
+                add (getSize (), classifier);
+            }
+        }
+        else {
+            // Try to move the classifier one step in the desired direction.
+            int index = indexOf (classifier);
+            ///ystem.err.println("Index of " + classifier + " = " + index);
+            if(direction) {
+                index--;
+                if(index < 0) {
+                    String args[] = new String[2];
+                    args[0] = Dictionary.get ("CDM.ClassifierManager.Classifier");
+                    args[1] = classifier.getName ();
+                    JOptionPane.showMessageDialog (Gatherer.g_man, Dictionary.get ("CDM.Move.At_Top", args), Dictionary.get ("CDM.Move.Title"), JOptionPane.ERROR_MESSAGE);
+                    return;
+                }
+                remove (classifier);
+                add (index, classifier);
+            }
+            else {
+                index++;
+                if(index >= getSize ()) {
+                    String args[] = new String[2];
+                    args[0] = Dictionary.get ("CDM.ClassifierManager.Classifier_Str");
+                    args[1] = classifier.getName ();
+                    JOptionPane.showMessageDialog (Gatherer.g_man, Dictionary.get ("CDM.Move.At_Bottom", args), Dictionary.get ("CDM.Move.Title"), JOptionPane.ERROR_MESSAGE);
+                    return;
+                }
+                remove (classifier);
+                add (index, classifier);
+            }
+        }
+        
+        // tell the format manager to update the names of its format statements
+        Gatherer.c_man.getCollection ().cdm.format_manager.refresh ();
+    }
+    
+    /** This method removes an assigned classifier. I was tempted to call it unassign, but remove is more consistant. Note that there is no way to remove a classifier from the library.
+     * @param classifier The Classifier to remove
+     * @see org.greenstone.gatherer.cdm.DynamicListModel
+     */
+    private void removeClassifier (Classifier classifier) {
+        remove (classifier);
+        // tell the format manager to update the names of its format statements
+        Gatherer.c_man.getCollection ().cdm.format_manager.refresh ();
+        
+    }
+    // When remove, move, and add(assign) classifiers, this method has to be called to get the feature combobox
+    // on the 'format feature' panel of the 'format' panel up to date.
+//    private void informFormatManager() {
+//        //this is really to call the gainFocus() method
+//        CollectionDesignManager.format_manager.getControls();
+//    }
+    
+    /** A class which provides controls for assigned and editing classifiers. */
+    private class ClassifierControl
+    extends JPanel
+    implements Control, DatabaseTypeManager.DatabaseTypeListener {
+        /** A combobox containing all of the known classifiers, including those that may have already been assigned. */
+        private JComboBox classifier_combobox = null;
+        /** Button for adding classifiers. */
+        private JButton add = null;
+        /** Button for configuring the selected classifier. */
+        private JButton configure = null;
+        private JButton move_down_button;
+        private JButton move_up_button;
+        
+        /** Button to remove the selected classifier. */
+        private JButton remove = null;
+
+        private JPanel database_type_panel = (JPanel)database_type_manager.getControls();
+        
+        /** A list of assigned classifiers. */
+        private JList classifier_list = null;
+        
+        /** Constructor.
+         * @see org.greenstone.gatherer.cdm.ClassifierManager.ClassifierControl.AddListener
+         * @see org.greenstone.gatherer.cdm.ClassifierManager.ClassifierControl.ConfigureListener
+         * @see org.greenstone.gatherer.cdm.ClassifierManager.ClassifierControl.RemoveListener
+         */
+        public ClassifierControl () {
+            // Create
+            this.setComponentOrientation(Dictionary.getOrientation());
+            add = new GLIButton (Dictionary.get ("CDM.ClassifierManager.Add"), Dictionary.get ("CDM.ClassifierManager.Add_Tooltip"));
+            
+            JPanel button_pane = new JPanel ();
+            button_pane.setComponentOrientation(Dictionary.getOrientation());
+            JPanel central_pane = new JPanel ();
+            central_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+            configure = new GLIButton (Dictionary.get ("CDM.ClassifierManager.Configure"), Dictionary.get ("CDM.ClassifierManager.Configure_Tooltip"));
+            configure.setEnabled (false);
+            
+            JPanel header_pane = new DesignPaneHeader ("CDM.GUI.Classifiers", "classifiers");
+            
+            ClassifierComboboxListener ccl = new ClassifierComboboxListener ();
+            classifier_combobox = new JComboBox (getAvailableClassifiers ());
+            classifier_combobox.setComponentOrientation(Dictionary.getOrientation());
+            classifier_combobox.setOpaque (!Utility.isMac ());
+            classifier_combobox.setEditable (false);
+            if(classifier_combobox.getItemCount () > 0) {
+		if (default_classifier!=null) {
+		    classifier_combobox.setSelectedItem(default_classifier);
+		} else {
+		    classifier_combobox.setSelectedIndex (0);
+		}
+                ccl.itemStateChanged (new ItemEvent (classifier_combobox, 0, null, ItemEvent.SELECTED));
+            }
+            
+            JLabel classifier_label = new JLabel (Dictionary.get ("CDM.ClassifierManager.Classifier"));
+            classifier_label.setComponentOrientation(Dictionary.getOrientation());
+            
+            classifier_list = new JList (model);
+            classifier_list.setComponentOrientation(Dictionary.getOrientation());
+            
+            classifier_list.setOpaque (true);
+            classifier_list.setSelectionMode (ListSelectionModel.SINGLE_SELECTION);
+            JLabel classifier_list_label = new JLabel (Dictionary.get ("CDM.ClassifierManager.Assigned"));
+            classifier_list_label.setComponentOrientation(Dictionary.getOrientation());
+            
+            classifier_list_label.setOpaque (true);
+            
+            JPanel classifier_list_pane = new JPanel ();
+            classifier_list_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+            JPanel classifier_pane = new JPanel ();
+            classifier_pane.setComponentOrientation(Dictionary.getOrientation());
+            remove = new GLIButton (Dictionary.get ("CDM.ClassifierManager.Remove"), Dictionary.get ("CDM.ClassifierManager.Remove_Tooltip"));
+            remove.setEnabled (false);
+            
+            JPanel temp = new JPanel (new BorderLayout ());
+            temp.setComponentOrientation(Dictionary.getOrientation());
+            
+            JPanel move_button_pane = new JPanel ();
+            move_button_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+            move_up_button = new GLIButton (Dictionary.get ("CDM.Move.Move_Up"), JarTools.getImage ("arrow-up.gif"), Dictionary.get ("CDM.Move.Move_Up_Tooltip"));
+            move_up_button.setEnabled (false);
+            
+            move_down_button = new GLIButton (Dictionary.get ("CDM.Move.Move_Down"), JarTools.getImage ("arrow-down.gif"), Dictionary.get ("CDM.Move.Move_Down_Tooltip"));
+            move_down_button.setEnabled (false);
+            
+            // Listeners
+            add.addActionListener (new AddListener ());
+            classifier_combobox.addItemListener (ccl);
+            configure.addActionListener (new ConfigureListener ());
+            remove.addActionListener (new RemoveListener ());
+            remove.addActionListener (CollectionDesignManager.buildcol_change_listener);
+            classifier_list.addMouseListener (new ClickListener ());
+            classifier_list.addListSelectionListener (new ListListener ());
+            ccl = null;
+            
+            MoveListener ml = new MoveListener ();
+            move_down_button.addActionListener (ml);
+            move_down_button.addActionListener (CollectionDesignManager.buildcol_change_listener);
+            move_up_button.addActionListener (ml);
+            move_up_button.addActionListener (CollectionDesignManager.buildcol_change_listener);
+            
+            // Layout
+            JPanel tmp;
+            move_button_pane.setLayout (new GridLayout (4,1));
+            move_button_pane.add (move_up_button);
+            tmp = new JPanel ();
+            tmp.setComponentOrientation(Dictionary.getOrientation());
+            move_button_pane.add (tmp);
+            tmp = new JPanel ();
+            tmp.setComponentOrientation(Dictionary.getOrientation());
+            move_button_pane.add (tmp);
+            move_button_pane.add (move_down_button);
+            
+            classifier_list_label.setBorder (BorderFactory.createEmptyBorder (0,2,0,2));
+            
+            classifier_list_pane.setLayout (new BorderLayout ());
+            classifier_list_pane.add (classifier_list_label, BorderLayout.NORTH);
+            classifier_list_pane.add (new JScrollPane (classifier_list), BorderLayout.CENTER);
+            classifier_list_pane.add (move_button_pane, BorderLayout.LINE_END);
+            
+            classifier_label.setBorder (BorderFactory.createEmptyBorder (0,0,5,0));
+            
+            classifier_pane.setBorder (BorderFactory.createEmptyBorder (5,0,5,0));
+            classifier_pane.setLayout (new BorderLayout (5,0));
+            classifier_pane.add (classifier_label, BorderLayout.LINE_START);
+            classifier_pane.add (classifier_combobox, BorderLayout.CENTER);
+            
+            button_pane.setLayout (new GridLayout (1, 3));
+            button_pane.add (add);
+            button_pane.add (configure);
+            button_pane.add (remove);
+            
+            temp.add (classifier_pane, BorderLayout.NORTH);
+            temp.add (button_pane, BorderLayout.SOUTH);
+            
+            central_pane.setBorder (BorderFactory.createEmptyBorder (5,0,0,0));
+            central_pane.setLayout (new BorderLayout ());
+            central_pane.add (database_type_panel, BorderLayout.NORTH);
+            central_pane.add (classifier_list_pane, BorderLayout.CENTER);
+            central_pane.add (temp, BorderLayout.SOUTH);
+            
+            setBorder (BorderFactory.createEmptyBorder (0,5,0,0));
+            setLayout (new BorderLayout ());
+            add (header_pane, BorderLayout.NORTH);
+            add (central_pane, BorderLayout.CENTER);
+        }
+        
+        /** Method which acts like a destructor, tidying up references to persistant objects.
+         */
+        public void destroy () {
+            add = null;
+            classifier_combobox = null;
+            classifier_list = null;
+            configure = null;
+            //instructions = null;
+            remove = null;
+        }
+        
+        public void gainFocus () {
+        }
+        
+        public void loseFocus () {
+        }
+        
+
+	public void databaseTypeChanged(String new_database_type)
+	{
+	    if(database_type.equals(new_database_type)){
+		return;
+	    }
+	    database_type = new_database_type;
+	}
+        
+        private class AddListener
+        implements ActionListener {
+            public void actionPerformed (ActionEvent event) {
+                if (classifier_combobox.getSelectedItem () != null) {
+                    // This must be done on a new thread for the remote building code
+                    new AddClassifierTask (classifier_combobox.getSelectedItem ().toString ()).start ();
+                }
+            }
+        }
+        
+        
+        private class AddClassifierTask
+        extends Thread {
+            private String classifier_name;
+            
+            public AddClassifierTask (String classifier_name) {
+                this.classifier_name = classifier_name;
+            }
+            
+            public void run () {
+                // Retrieve the classifier
+                Classifier classifier = Classifiers.getClassifier (classifier_name, true);
+                if (classifier == null) {
+                    System.err.println ("Error: getClassifier() returned null.");
+                    return;
+                }
+                
+                // Create a new element in the DOM
+                Element new_classifier_element = CollectionConfiguration.createElement (StaticStrings.CLASSIFY_ELEMENT);
+                new_classifier_element.setAttribute (StaticStrings.TYPE_ATTRIBUTE, classifier.getName ());
+                
+                Classifier new_classifier = new Classifier (new_classifier_element, classifier);
+                
+                ArgumentConfiguration ac = new ArgumentConfiguration (new_classifier);
+                ac.addOKButtonActionListener (CollectionDesignManager.buildcol_change_listener);
+                if (ac.display ()) {
+                    assignClassifier (new_classifier);
+                    classifier_list.setSelectedValue (new_classifier, true);
+                }
+            }
+        }
+        
+        
+        /** This listener reacts to changes in the current selection of the classifier combobox. */
+        private class ClassifierComboboxListener
+        implements ItemListener {
+            /** When a user selects a certain classifier, update the tooltip to show the classifier description. */
+            public void itemStateChanged (ItemEvent event) {
+                if(event.getStateChange () == ItemEvent.SELECTED) {
+                    // Retrieve the selected classifier
+                    Classifier current_selection = (Classifier) classifier_combobox.getSelectedItem ();
+                    // And reset the tooltip.
+                    classifier_combobox.setToolTipText (Utility.formatHTMLWidth (current_selection.getDescription (), 40));
+                    current_selection = null;
+                }
+            }
+        }
+        
+        
+        /** Listens for double clicks apon the list and react as if the configure button was pushed. */
+        private class ClickListener
+        extends MouseAdapter {
+            /** Called whenever the mouse is clicked over a registered component, we use this to chain through to the configure prompt.
+             * @param event A <strong>MouseEvent</strong> containing information about the mouse click.
+             */
+            public void mouseClicked (MouseEvent event) {
+                if(event.getClickCount () == 2 ) {
+                    if(!classifier_list.isSelectionEmpty ()) {
+                        Classifier classifier = (Classifier) classifier_list.getSelectedValue ();
+                        ArgumentConfiguration ac = new ArgumentConfiguration (classifier);
+                        ac.addOKButtonActionListener (CollectionDesignManager.buildcol_change_listener);
+                        if (ac.display ()) {
+                            refresh (classifier);
+                        }
+                        ac.destroy ();
+                        ac = null;
+                    }
+                }
+            }
+        }
+        
+        /** This class listens for actions upon the configure button in the controls, and if detected creates a new ArgumentConfiguration dialog box to allow for configuration.
+         */
+        private class ConfigureListener
+        implements ActionListener {
+            /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured on one of our target controls.
+             * @param event An <strong>ActionEvent</strong> containing information garnered from the control action.
+             * @see org.greenstone.gatherer.cdm.ArgumentConfiguration
+             * @see org.greenstone.gatherer.cdm.Classifier
+             */
+            public void actionPerformed (ActionEvent event) {
+                if(!classifier_list.isSelectionEmpty ()) {
+                    Classifier classifier = (Classifier) classifier_list.getSelectedValue ();
+                    ArgumentConfiguration ac = new ArgumentConfiguration (classifier);
+                    ac.addOKButtonActionListener (CollectionDesignManager.buildcol_change_listener);
+                    if (ac.display ()) {
+                        refresh (classifier);
+                    }
+                    ac.destroy ();
+                    ac = null;
+                }
+            }
+        }
+        
+        /** listens for changes in the list selection and enables the configure and remove buttons if there is a selection, disables them if there is no selection */
+        private class ListListener
+        implements ListSelectionListener {
+            
+            public void valueChanged (ListSelectionEvent e) {
+                if (!e.getValueIsAdjusting ()) { // we get two events for one change in list selection - use the false one ( the second one)
+                    if (classifier_list.isSelectionEmpty ()) {
+                        move_up_button.setEnabled (false);
+                        move_down_button.setEnabled (false);
+                        configure.setEnabled (false);
+                        remove.setEnabled (false);
+                    }
+                    else {
+                        configure.setEnabled (true);
+                        remove.setEnabled (true);
+                        int selected_index = classifier_list.getSelectedIndex ();
+                        move_up_button.setEnabled (selected_index !=0);
+                        move_down_button.setEnabled (selected_index != model.getSize ()-1);
+                    }
+                }
+            }
+        }
+        
+        /** Listens for actions apon the move buttons in the manager controls, and if detected calls the <i>moveClassifier()</i> method of the manager with the appropriate details. */
+        private class MoveListener
+        implements ActionListener {
+            /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured on one of our target controls.
+             * @param event An <strong>ActionEvent</strong> containing information garnered from the control action.
+             */
+            public void actionPerformed (ActionEvent event) {
+                if(!classifier_list.isSelectionEmpty ()) {
+                    Object object = classifier_list.getSelectedValue ();
+                    if(object instanceof Classifier) {
+                        Classifier classifier = (Classifier) object;
+                        if(event.getSource () == move_up_button) {
+                            moveClassifier (classifier, true, false);
+                        }
+                        else if(event.getSource () == move_down_button) {
+                            moveClassifier (classifier, false, false);
+                        }
+                        classifier_list.setSelectedValue (classifier, true);
+                    }
+                }
+            }
+        }
+        
+        /** This class listens for actions upon the remove button in the controls, and if detected calls the <i>removeClassifier()</i> method.
+         */
+        private class RemoveListener
+        implements ActionListener {
+            /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured on one of our target controls.
+             * @param event An <strong>ActionEvent</strong> containing information garnered from the control action.
+             */
+            public void actionPerformed (ActionEvent event) {
+                if(classifier_list.isSelectionEmpty ()) {
+                    remove.setEnabled (false);
+                    return;
+                }
+                int selected_index = classifier_list.getSelectedIndex ();
+                Object selected_classifier = classifier_list.getSelectedValue ();
+                if (!(selected_classifier instanceof Classifier)) {
+                    return; // what else could we have here???
+                }
+                removeClassifier ((Classifier)selected_classifier);
+                
+                if (selected_index >= classifier_list.getModel ().getSize ()) {
+                    selected_index--;
+                }
+                if (selected_index >=0) {
+                    classifier_list.setSelectedIndex (selected_index);
+                } else {
+                    // no more classifiers in the list
+                    remove.setEnabled (false);
+                }
+            }
+        }
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectCfgReadWrite.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectCfgReadWrite.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectCfgReadWrite.java	(revision 31635)
@@ -0,0 +1,1333 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Methods to read collect.cfg files into internal XML form, and write 
+ * them back out again.
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.StringTokenizer;
+
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataTools;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.XMLTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+import org.w3c.dom.*;
+
+public class CollectCfgReadWrite {
+
+  
+  static public String toString (Element command_element) {
+    String command_element_name = command_element.getNodeName ();
+    if(command_element_name.equals (StaticStrings.CLASSIFY_ELEMENT)) {
+      return classifyToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.FORMAT_ELEMENT)) {
+      return formatToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.INDEXES_ELEMENT)) {
+      return indexesToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.INDEX_DEFAULT_ELEMENT)) {
+      return indexDefaultToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.LANGUAGES_ELEMENT)) {
+      return languagesToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.LANGUAGE_DEFAULT_ELEMENT)) {
+      return languageDefaultToString (command_element);
+    }
+    else if (command_element_name.equals (StaticStrings.LANGUAGE_METADATA_ELEMENT)) {
+      return languageMetadataToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.INDEXOPTIONS_ELEMENT)) {
+      return indexOptionsToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.INDEXOPTION_DEFAULT_ELEMENT)) {
+      return indexOptionDefaultToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.COLLECTIONMETADATA_ELEMENT)) {
+      return metadataToString (command_element); 
+    }
+    else if(command_element_name.equals (StaticStrings.COLLECTIONMETADATA_CREATOR_ELEMENT)) {
+      return metadataToString (command_element); 
+    }
+    else if(command_element_name.equals (StaticStrings.COLLECTIONMETADATA_MAINTAINER_ELEMENT)) {
+      return metadataToString (command_element); 
+    }
+    else if(command_element_name.equals (StaticStrings.COLLECTIONMETADATA_PUBLIC_ELEMENT)) {
+      return metadataToString (command_element); 
+    }
+    else if (command_element_name.equals (StaticStrings.BUILDTYPE_ELEMENT)) {
+      return metadataToString (command_element); 
+    }
+    else if (command_element_name.equals (StaticStrings.DATABASETYPE_ELEMENT)) {
+      return metadataToString (command_element); 
+    } 
+    else if(command_element_name.equals (StaticStrings.PLUGIN_ELEMENT)) {
+      return pluginToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.SUBCOLLECTION_ELEMENT)) {
+      return subcollectionToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.SUBCOLLECTION_DEFAULT_INDEX_ELEMENT)) {
+      return subcollectionDefaultIndexToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.SUBCOLLECTION_INDEXES_ELEMENT)) {
+      return subcollectionIndexesToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.SUPERCOLLECTION_ELEMENT)) {
+      return supercollectionToString (command_element);
+    }
+    else if(command_element_name.equals (StaticStrings.UNKNOWN_ELEMENT)) {
+      return unknownToString (command_element);
+    }
+    return "";
+  }
+
+  static private String classifyToString (Element command_element) {
+    StringBuffer text = new StringBuffer (StaticStrings.CLASSIFY_STR);
+    text.append (StaticStrings.TAB_CHARACTER);
+    text.append (command_element.getAttribute (StaticStrings.TYPE_ATTRIBUTE));
+    NodeList option_elements = command_element.getElementsByTagName (StaticStrings.OPTION_ELEMENT);
+    int option_elements_length = option_elements.getLength ();
+    for(int j = 0; j < option_elements_length; j++) {
+      Element option_element = (Element) option_elements.item (j);
+      if(option_element.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.TRUE_STR)) {
+	text.append (StaticStrings.SPACE_CHARACTER);
+	text.append (StaticStrings.MINUS_CHARACTER);
+	text.append (option_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+	String value_str = XMLTools.getValue (option_element);
+	
+	if (value_str.length () > 0) {
+	  text.append (StaticStrings.SPACE_CHARACTER);
+	  if(value_str.indexOf (StaticStrings.SPACE_CHARACTER) != -1) {
+	    // enclose in quotes
+	    text.append(StaticStrings.SPEECH_CHARACTER);
+	    text.append(value_str);
+	    text.append(StaticStrings.SPEECH_CHARACTER);
+	  } else {
+		    
+	    text.append(value_str);
+	  }
+	}
+              
+	value_str = null;
+      }
+      option_element = null;
+    }
+    option_elements = null;
+    return text.toString ();
+  }
+    
+  static private String formatToString (Element command_element) {
+    StringBuffer text = new StringBuffer (StaticStrings.FORMAT_STR);
+    text.append (StaticStrings.SPACE_CHARACTER);
+    text.append (command_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+    text.append (StaticStrings.SPACE_CHARACTER);
+    String value_str = command_element.getAttribute (StaticStrings.VALUE_ATTRIBUTE);
+    if(value_str.length () != 0) {
+      text.append (value_str);
+    }
+    else {
+      // Remember to encode format string to Greenstone specification
+      value_str = Codec.transform (XMLTools.getValue (command_element), Codec.DOM_TO_GREENSTONE);
+      text.append (StaticStrings.SPEECH_CHARACTER);
+      text.append (value_str);
+      text.append (StaticStrings.SPEECH_CHARACTER);
+    }
+    value_str = null;
+    return text.toString ();
+  }
+  
+  static private String indexesToString (Element command_element) {
+    boolean comment_only = false;
+    StringBuffer text = new StringBuffer ("");
+    if(command_element.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.FALSE_STR)) {
+      text.append ("#");
+      comment_only = true;
+    }
+    text.append (StaticStrings.INDEX_STR);
+    text.append (StaticStrings.TAB_CHARACTER);
+    if(!comment_only) {
+      text.append (StaticStrings.TAB_CHARACTER);
+    }
+    NodeList index_elements = command_element.getElementsByTagName (StaticStrings.INDEX_ELEMENT);
+    if (index_elements.getLength () == 0) { // no indexes
+      return "";
+    }
+    // For each index, write its level, a colon, then concatenate its child content elements into a single comma separated list
+    int index_elements_length = index_elements.getLength ();
+    for(int j = 0; j < index_elements_length; j++) {
+      Element index_element = (Element) index_elements.item (j);
+      String level_str = index_element.getAttribute (StaticStrings.LEVEL_ATTRIBUTE);
+      if(level_str.length () > 0) {
+	text.append (level_str);
+	text.append (StaticStrings.COLON_CHARACTER);
+      }
+      NodeList content_elements = index_element.getElementsByTagName (StaticStrings.CONTENT_ELEMENT);
+      int content_elements_length = content_elements.getLength ();
+      // Don't output anything if no indexes are set
+      if(content_elements_length == 0) {
+	return null;
+      }
+      for(int k = 0; k < content_elements_length; k++) {
+	Element content_element = (Element) content_elements.item (k);
+	String name_str = content_element.getAttribute (StaticStrings.NAME_ATTRIBUTE);
+	text.append (name_str);
+	name_str = null;
+	if(k < content_elements_length - 1) {
+	  text.append (StaticStrings.COMMA_CHARACTER);
+	}
+	content_element = null;
+      }
+      if(j < index_elements_length - 1) {
+	text.append (StaticStrings.SPACE_CHARACTER);
+      }
+      content_elements = null;
+      index_element = null;
+    }
+    index_elements = null;
+    return text.toString ();
+  }
+    
+  static private String indexDefaultToString (Element command_element) {
+    StringBuffer text = new StringBuffer ("");
+    if(command_element.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.FALSE_STR)) {
+      text.append ("#");
+    }
+    text.append (StaticStrings.INDEX_DEFAULT_STR);
+    text.append (StaticStrings.TAB_CHARACTER);
+    if (!command_element.getAttribute (StaticStrings.LEVEL_ATTRIBUTE).equals ("")) {
+      text.append (command_element.getAttribute (StaticStrings.LEVEL_ATTRIBUTE));
+      text.append (StaticStrings.COLON_CHARACTER);
+    }
+    NodeList content_elements = command_element.getElementsByTagName (StaticStrings.CONTENT_ELEMENT);
+    int content_elements_length = content_elements.getLength ();
+    for(int j = 0; j < content_elements_length; j++) {
+      Element content_element = (Element) content_elements.item (j);
+      String name_str = content_element.getAttribute (StaticStrings.NAME_ATTRIBUTE);
+      text.append (name_str);
+      name_str = null;
+      if(j < content_elements_length - 1) {
+	text.append (StaticStrings.COMMA_CHARACTER);
+      }
+      content_element = null;
+    }
+    content_elements = null;
+    return text.toString ();
+  }
+    
+  static private String languagesToString (Element command_element) {
+    StringBuffer text = new StringBuffer (StaticStrings.LANGUAGES_STR);
+    text.append (StaticStrings.TAB_CHARACTER);
+    // Retrieve all the languages and write them out in a space separated list
+    NodeList language_elements = command_element.getElementsByTagName (StaticStrings.LANGUAGE_ELEMENT);
+    int language_elements_length = language_elements.getLength ();
+    if(language_elements_length == 0) {
+      return null;
+    }
+    for(int j = 0; j < language_elements_length; j++) {
+      Element language_element = (Element) language_elements.item (j);
+      text.append (language_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+      if(j < language_elements_length - 1) {
+	text.append (StaticStrings.SPACE_CHARACTER);
+      }
+    }
+    return text.toString ();
+  }
+    
+  static private String languageDefaultToString (Element command_element) {
+    StringBuffer text = new StringBuffer (StaticStrings.LANGUAGE_DEFAULT_STR);
+    text.append (StaticStrings.TAB_CHARACTER);
+    text.append (command_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+    return text.toString ();
+  }
+    
+  static private String languageMetadataToString (Element command_element) {
+    if (!command_element.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.TRUE_STR)) {
+      return "";
+    }
+    StringBuffer text = new StringBuffer (StaticStrings.LANGUAGE_METADATA_STR);
+    text.append (StaticStrings.TAB_CHARACTER);
+    String name_str = command_element.getAttribute (StaticStrings.NAME_ATTRIBUTE);
+    text.append (name_str);
+    return text.toString ();
+  }
+    
+  static private String indexOptionsToString (Element command_element) {
+    StringBuffer text = new StringBuffer ("");
+    if(command_element.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.FALSE_STR)) {
+      text.append ("#");
+    }
+    text.append (command_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+    text.append (StaticStrings.TAB_CHARACTER);
+    NodeList content_elements = command_element.getElementsByTagName (StaticStrings.INDEXOPTION_ELEMENT);
+    int content_elements_length = content_elements.getLength ();
+    // Don't output anything if no options are set.
+    if(content_elements_length == 0) {
+      return null;
+    }
+    for(int i = 0; i < content_elements_length; i++) {
+      Element content_element = (Element) content_elements.item (i);
+      text.append (content_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+      text.append (StaticStrings.SPACE_CHARACTER);
+    }
+    return text.substring (0, text.length () - 1);
+  }
+    
+  static private String indexOptionDefaultToString (Element command_element) {
+    // Don't bother if there is no value
+    if (command_element.getAttribute (StaticStrings.VALUE_ATTRIBUTE).equals ("")) {
+      return "";
+    }
+    StringBuffer text = new StringBuffer ("");
+    if(command_element.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.FALSE_STR)) {
+      text.append ("#");
+    }
+    text.append (command_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+    text.append (StaticStrings.TAB_CHARACTER);
+    text.append (command_element.getAttribute (StaticStrings.VALUE_ATTRIBUTE));
+    return text.toString ();
+  }
+    
+  static private String metadataToString (Element command_element) {
+    // lets first check the value - if its empty, don't bother sticking it in the config file
+    String value_str = XMLTools.getValue (command_element);
+    if (value_str.equals ("")) {
+      return "";
+    }
+    boolean special = false;
+        
+    StringBuffer text = new StringBuffer ("");
+    String name_str = command_element.getAttribute (StaticStrings.NAME_ATTRIBUTE);
+    // If the name is one of the special four, we don't write the collectionmeta first. Note maintainer and buildtype are singled out for 'prittying' reasons.
+    if(name_str.equals (StaticStrings.COLLECTIONMETADATA_MAINTAINER_STR)|| name_str.equals (StaticStrings.BUILDTYPE_STR) || name_str.equals (StaticStrings.DATABASETYPE_STR)) {
+      text.append (name_str);
+      text.append (StaticStrings.TAB_CHARACTER);
+      special = true;
+    }
+    else if (name_str.equals (StaticStrings.COLLECTIONMETADATA_CREATOR_STR) || name_str.equals (StaticStrings.COLLECTIONMETADATA_PUBLIC_STR) ) {
+      text.append (name_str);
+      text.append (StaticStrings.TAB_CHARACTER);
+      text.append (StaticStrings.TAB_CHARACTER);
+      special = true;
+    }
+    else {
+      text.append (StaticStrings.COLLECTIONMETADATA_STR);
+      text.append (StaticStrings.TAB_CHARACTER);
+      text.append (name_str);
+      text.append (StaticStrings.SPACE_CHARACTER);
+      String language_str = command_element.getAttribute (StaticStrings.LANGUAGE_ATTRIBUTE);
+      text.append (StaticStrings.LBRACKET_CHARACTER);
+      text.append (StaticStrings.LANGUAGE_ARGUMENT);
+      text.append (language_str);
+      text.append (StaticStrings.RBRACKET_CHARACTER);
+      text.append (StaticStrings.SPACE_CHARACTER);
+    }
+    name_str = null;
+        
+    // decode the value from XML to a form for config file
+    value_str = Codec.transform (value_str, Codec.DOM_TO_GREENSTONE);
+        
+    // We don't wrap the email addresses in quotes, nor the other special metadata
+    if(special) {
+      text.append (value_str);
+    }
+    else {
+      text.append (StaticStrings.SPEECH_CHARACTER);
+      text.append (value_str);
+      text.append (StaticStrings.SPEECH_CHARACTER);
+    }
+    value_str = null;
+    return text.toString ();
+  }
+    
+  static private String searchtypeToString (Element command_element) {
+    if(command_element.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.TRUE_STR)) {
+      StringBuffer text = new StringBuffer (StaticStrings.SEARCHTYPE_STR);
+      text.append (StaticStrings.TAB_CHARACTER);
+      NodeList search_elements = command_element.getElementsByTagName (StaticStrings.CONTENT_ELEMENT);
+      int search_elements_length = search_elements.getLength ();
+      for(int i = 0; i < search_elements_length; i++) {
+	Element search_element = (Element) search_elements.item (i);
+	text.append (search_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+	text.append (StaticStrings.SPACE_CHARACTER);
+      }
+      return text.substring (0, text.length () - 1);
+    }
+    else {
+      return null;
+    }
+  }
+    
+  static private String subcollectionToString (Element command_element) {
+    StringBuffer text = new StringBuffer (StaticStrings.SUBCOLLECTION_STR);
+    text.append (StaticStrings.SPACE_CHARACTER);
+    text.append (command_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+    text.append (StaticStrings.SPACE_CHARACTER);
+    text.append (StaticStrings.TAB_CHARACTER);
+    text.append (StaticStrings.SPEECH_CHARACTER);
+    if(command_element.getAttribute (StaticStrings.TYPE_ATTRIBUTE).equals (StaticStrings.EXCLUDE_STR)) {
+      text.append (StaticStrings.EXCLAMATION_CHARACTER);
+    }
+    text.append (command_element.getAttribute (StaticStrings.CONTENT_ATTRIBUTE));
+    text.append (StaticStrings.SEPARATOR_CHARACTER);
+    text.append (XMLTools.getValue (command_element));
+    text.append (StaticStrings.SEPARATOR_CHARACTER);
+    String options_str = command_element.getAttribute (StaticStrings.OPTIONS_ATTRIBUTE);
+    if(options_str.length () > 0) {
+      text.append (options_str);
+    }
+    options_str = null;
+    text.append (StaticStrings.SPEECH_CHARACTER);
+    return text.toString ();
+  }
+    
+  static private String subcollectionDefaultIndexToString (Element command_element) {
+    StringBuffer text = new StringBuffer (StaticStrings.SUBCOLLECTION_DEFAULT_INDEX_STR);
+    text.append (StaticStrings.TAB_CHARACTER);
+    NodeList content_elements = command_element.getElementsByTagName (StaticStrings.CONTENT_ELEMENT);
+    int content_elements_length = content_elements.getLength ();
+    for(int j = 0; j < content_elements_length; j++) {
+      Element content_element = (Element) content_elements.item (j);
+      text.append (content_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+      if(j < content_elements_length - 1) {
+	text.append (StaticStrings.COMMA_CHARACTER);
+      }
+    }
+    return text.toString ();
+  }
+    
+  static private String subcollectionIndexesToString (Element command_element) {
+    StringBuffer text = new StringBuffer (StaticStrings.SUBCOLLECTION_INDEX_STR);
+    text.append (StaticStrings.TAB_CHARACTER);
+    // Retrieve all of the subcollection index partitions
+    NodeList subcollectionindex_elements = command_element.getElementsByTagName (StaticStrings.INDEX_ELEMENT);
+    int subcollectionindex_elements_length = subcollectionindex_elements.getLength ();
+    if(subcollectionindex_elements_length == 0) {
+      return null;
+    }
+    for(int j = 0; j < subcollectionindex_elements_length; j++) {
+      Element subcollectionindex_element = (Element) subcollectionindex_elements.item (j);
+      NodeList content_elements = subcollectionindex_element.getElementsByTagName (StaticStrings.CONTENT_ELEMENT);
+      int content_elements_length = content_elements.getLength ();
+      for(int k = 0; k < content_elements_length; k++) {
+	Element content_element = (Element) content_elements.item (k);
+	text.append (content_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+	if(k < content_elements_length - 1) {
+	  text.append (StaticStrings.COMMA_CHARACTER);
+	}
+      }
+      if(j < subcollectionindex_elements_length - 1) {
+	text.append (StaticStrings.SPACE_CHARACTER);
+      }
+    }
+    return text.toString ();
+  }
+    
+  static private String supercollectionToString (Element command_element) {
+    NodeList content_elements = command_element.getElementsByTagName (StaticStrings.COLLECTION_ELEMENT);
+    int content_elements_length = content_elements.getLength ();
+    if(content_elements_length > 1) {
+      StringBuffer text = new StringBuffer (StaticStrings.SUPERCOLLECTION_STR);
+      text.append (StaticStrings.TAB_CHARACTER);
+      for(int j = 0; j < content_elements_length; j++) {
+	Element content_element = (Element) content_elements.item (j);
+	text.append (content_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+	if(j < content_elements_length - 1) {
+	  text.append (StaticStrings.SPACE_CHARACTER);
+	}
+      }
+      return text.toString ();
+    }
+    return null;
+  }
+    
+  static private String unknownToString (Element command_element) {
+    return XMLTools.getValue (command_element);
+  }
+    
+
+  /** Parse a collect.cfg into a DOM model representation.
+   * note we are ignoring 2.39 compatibility now. */
+  static public String parse (File collect_cfg_file, Document document) {
+    // hack for pre 2.71 compatibility - we need to add in a
+    // build type if there is not one there
+    boolean search_types_parsed = false;
+    boolean build_types_parsed = false;
+    try {
+      StringBuffer saved_collect_cfg_string_buffer = new StringBuffer ();
+            
+      Element collect_cfg_element = document.getDocumentElement ();
+      // Read in the file one command at a time.
+      InputStream istream = new FileInputStream (collect_cfg_file);
+      Reader in_reader = new InputStreamReader (istream, CollectionConfiguration.ENCODING);
+      BufferedReader in = new BufferedReader (in_reader);
+      String command_str = null;
+      while((command_str = in.readLine ()) != null) {
+	saved_collect_cfg_string_buffer.append (command_str + "\n");
+                
+	boolean append_element = true;
+	Element command_element = null;
+	// A command may be broken over several lines.
+	command_str = command_str.trim ();
+	boolean eof = false;
+	while(!eof && command_str.endsWith (StaticStrings.NEWLINE_CHARACTER)) {
+	  String next_line = in.readLine ();
+	  if(next_line != null) {
+	    next_line = next_line.trim ();
+	    if(next_line.length () > 0) {
+	      // Remove the new line character
+	      command_str = command_str.substring (0, command_str.lastIndexOf (StaticStrings.NEWLINE_CHARACTER));
+	      // And append the next line, which due to the test above must be non-zero length
+	      command_str = command_str + next_line;
+	    }
+	    next_line = null;
+	  }
+	  // If we've reached the end of the file theres nothing more we can do
+	  else {
+	    eof = true;
+	  }
+	}
+	// If there is still a new line character, then we remove it and hope for the best
+	if(command_str.endsWith (StaticStrings.NEWLINE_CHARACTER)) {
+	  command_str = command_str.substring (0, command_str.lastIndexOf (StaticStrings.NEWLINE_CHARACTER));
+	}
+	// Now we've either got a command to parse...
+	if(command_str.length () != 0) {
+	  // Start trying to figure out what it is
+	  //StringTokenizer tokenizer = new StringTokenizer(command_str);
+	  // Instead of a standard string tokenizer I'm going to use the new version of CommandTokenizer, which is not only smart enough to correctly notice speech marks and correctly parse them out, but now also takes the input stream so it can rebuild tokens that stretch over several lines.
+	  CommandTokenizer tokenizer = new CommandTokenizer (command_str, in);
+	  String command_type = tokenizer.nextToken ().toLowerCase ();
+	  // Why can't you switch on strings eh? We pass it to the various subparsers who each have a bash at parsing the command. If none can parse the command, an unknown element is created
+	  if(command_element == null && command_type.equals (StaticStrings.CLASSIFY_STR)) {
+	    command_element = parseClassify (command_str, document); 
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.FORMAT_STR)) {
+	    command_element = parseFormat (tokenizer, document); // Revised to handle multiple lines
+	  }
+	  if(command_element == null && (command_type.equals (StaticStrings.INDEX_STR)  || command_type.equals (StaticStrings.COMMENTED_INDEXES_STR))) {
+	    command_element = parseIndex (command_str, document);
+	  }
+	  if(command_element == null && (command_type.equals (StaticStrings.INDEX_DEFAULT_STR) || command_type.equals (StaticStrings.COMMENTED_INDEX_DEFAULT_STR))) {
+                        
+	    command_element = parseIndexDefault (command_str, document);
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.LANGUAGES_STR)) {
+	    command_element = parseLanguage (command_str, document);
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.LANGUAGE_DEFAULT_STR)) {
+	    command_element = parseLanguageDefault (command_str, document);
+	  }
+	  if (command_element == null && command_type.equals (StaticStrings.LANGUAGE_METADATA_STR)) {
+	    command_element = parseLanguageMetadata (command_str, document);
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.LEVELS_STR)) {
+	    command_element = parseIndexOptions (command_str, document, StaticStrings.LEVELS_STR, true);
+	  }
+	  if (command_element == null && command_type.equals (StaticStrings.COMMENTED_LEVELS_STR)) {
+	    command_element = parseIndexOptions (command_str, document, StaticStrings.LEVELS_STR, false);
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.LEVEL_DEFAULT_STR)) {
+	    command_element = parseIndexOptionDefault (command_str, document, StaticStrings.LEVEL_DEFAULT_STR, true);
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.COMMENTED_LEVEL_DEFAULT_STR)) {
+	    command_element = parseIndexOptionDefault (command_str, document, StaticStrings.LEVEL_DEFAULT_STR, false);
+	  }
+	  if (command_element == null && command_type.equals (StaticStrings.INDEXOPTIONS_STR)) {
+	    command_element = parseIndexOptions (command_str, document, StaticStrings.INDEXOPTIONS_STR, true);
+	  }
+	  if (command_element == null && command_type.equals (StaticStrings.COMMENTED_INDEXOPTIONS_STR)) {
+	    command_element = parseIndexOptions (command_str, document, StaticStrings.INDEXOPTIONS_STR, false);
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.COLLECTIONMETADATA_STR)) {
+	    command_element = parseMetadata (tokenizer, document); // Revised to handle multiple lines
+	  }
+	  if(command_element == null && (command_type.equals (StaticStrings.COLLECTIONMETADATA_PUBLIC_STR) || command_type.equals (StaticStrings.COLLECTIONMETADATA_CREATOR_STR) || command_type.equals (StaticStrings.COLLECTIONMETADATA_MAINTAINER_STR) || command_type.equals (StaticStrings.BUILDTYPE_STR) || command_type.equals (StaticStrings.DATABASETYPE_STR))) {
+	    command_element = parseMetadataSpecial (command_str, document);
+	    // pre 2.71 hack
+	    if (command_type.equals (StaticStrings.BUILDTYPE_STR)) {
+	      build_types_parsed = true;
+	    }
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.PLUGIN_STR)) {
+	    command_element = parsePlugin (command_str, document);
+	  }
+	  // leave here for backwards compatibility
+	  if(command_element == null && command_type.equals (StaticStrings.SEARCHTYPE_STR)) {
+	    command_element = parseSearchType (command_str, document);
+	    // pre 2.71 hack
+	    search_types_parsed = true;
+                        
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.SUBCOLLECTION_STR)) {
+	    command_element = parseSubCollection (command_str, document);
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.SUBCOLLECTION_DEFAULT_INDEX_STR)) {
+	    command_element = parseSubCollectionDefaultIndex (command_str, document);
+	  }
+	  if(command_element == null && command_type.equals (StaticStrings.SUBCOLLECTION_INDEX_STR)) {
+	    command_element = parseSubCollectionIndex (command_str, document);
+	  }
+	  if(command_element == null && (command_type.equals (StaticStrings.SUPERCOLLECTION_STR) || command_type.equals (StaticStrings.CCS_STR))) {
+	    command_element = parseSuperCollection (command_str, document);
+	  }
+	  // Doesn't match any known type
+	  command_type = null;
+	  if(command_element == null) {
+	    // No-one knows what to do with this command, so we create an Unknown command element
+	    command_element = document.createElement (StaticStrings.UNKNOWN_ELEMENT);
+	    XMLTools.setValue (command_element, command_str);
+	  }
+	}
+	// Or an empty line to remember for later
+	else {
+	  command_element = document.createElement (CollectionConfiguration.NEWLINE_ELEMENT);
+	}
+	// Now command element shouldn't be null so we append it to the collection config DOM, but only if we haven't been told not to add it
+	//if(append_element) {
+	collect_cfg_element.appendChild (command_element);
+	//}
+      }
+      if (!build_types_parsed) {
+	String buildtype_type = BuildTypeManager.BUILD_TYPE_MG;
+	if (search_types_parsed) {
+	  buildtype_type = BuildTypeManager.BUILD_TYPE_MGPP;
+	}
+	Element command_element = parseMetadataSpecial (StaticStrings.BUILDTYPE_STR+"  "+buildtype_type, document);
+	Node target_node = CollectionConfiguration.findInsertionPoint (command_element);
+	if(target_node != null) {
+	  collect_cfg_element.insertBefore (command_element, target_node);
+	}
+	else {
+	  collect_cfg_element.appendChild (command_element);
+	}
+                
+      }
+      return saved_collect_cfg_string_buffer.toString();
+    }
+    catch(Exception exception) {
+      DebugStream.println ("Error in CollectionConfiguration.parse(java.io.File): " + exception);
+      DebugStream.printStackTrace (exception);
+    }
+
+    return null;
+  }
+    
+    
+  /** Parses arguments from a tokenizer and returns a HashMap of mappings. The tricky bit here is that not all entries in the HashMap are name->value pairs, as some arguments are boolean and are turned on by their presence. Arguments are denoted by a '-' prefix.
+   * @param tokenizer a CommandTokenizer based on the unconsumed portion of a command string
+   * @return a HashMap containing the arguments parsed
+   */
+  static public HashMap parseArguments (CommandTokenizer tokenizer) {
+    HashMap arguments = new HashMap ();
+    String name = null;
+    String value = null;
+    while(tokenizer.hasMoreTokens () || name != null) {
+      // First we retrieve a name if we need one.
+      if(name == null) {
+	name = tokenizer.nextToken ();
+      }
+      // Now we attempt to retrieve a value
+      if(tokenizer.hasMoreTokens ()) {
+	value = tokenizer.nextToken ();
+	// Test if the value is actually a name, and if so add the name by itself, then put value into name so that it is parsed correctly during the next loop.
+	// The value is not a name if it contains a space character: it's a quoted value
+	if (value.startsWith(StaticStrings.MINUS_CHARACTER) && value.indexOf(StaticStrings.SPACE_CHARACTER) == -1) {
+	  arguments.put (name, null);
+	  name = value;
+	}
+	// Otherwise we have a typical name->value pair ready to go
+	else {
+	  arguments.put (name, value);
+	  name = null;
+	}
+      }
+      // Otherwise its a binary flag
+      else {
+	arguments.put (name, null);
+	name = null;
+      }
+    }
+    return arguments;
+  }
+    
+  static private Element parseClassify (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      CommandTokenizer tokenizer = new CommandTokenizer (command_str);
+      // Check the token count. The token count from a command tokenizer isn't guarenteed to be correct, but it does give the maximum number of available tokens according to the underlying StringTokenizer (some of which may actually be append together by the CommandTokenizer as being a single argument).
+      if(tokenizer.countTokens () >= 2) {  // Must support "classify Phind" (no args)
+	command_element = document.createElement (StaticStrings.CLASSIFY_ELEMENT);
+	// First token is classify
+	tokenizer.nextToken ();
+	// The next token is the classifier type
+	command_element.setAttribute (StaticStrings.TYPE_ATTRIBUTE, tokenizer.nextToken ());
+	// Now we parse out the remaining arguments into a hashmapping from name to value
+	HashMap arguments = parseArguments (tokenizer);
+	// Assign the arguments as Option elements
+	Iterator names = arguments.keySet ().iterator ();
+	while(names.hasNext ()) {
+	  String name = (String) names.next ();
+	  String value = (String) arguments.get (name); // Can be null
+	  Element option_element = document.createElement (StaticStrings.OPTION_ELEMENT);
+	  option_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, name.substring (1));
+	  if(value != null) {
+	    // Remove any speech marks appended in strings containing whitespace
+	    if(value.startsWith (StaticStrings.SPEECH_CHARACTER) && value.endsWith (StaticStrings.SPEECH_CHARACTER)) {
+	      value = value.substring (1, value.length () - 1);
+	    }
+	    XMLTools.setValue (option_element, value);
+	  }
+	  option_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+	  command_element.appendChild (option_element);
+	  option_element = null;
+	  name = null;
+	  value = null;
+	}
+	names = null;
+	arguments = null;
+      }
+      tokenizer = null;
+    }
+    catch(Exception error) {
+    }
+    return command_element;
+  }
+    
+  static private Element parseFormat (CommandTokenizer tokenizer, Document document) {
+    Element command_element = null;
+    try {
+      command_element = document.createElement (StaticStrings.FORMAT_ELEMENT);
+      String name_str = tokenizer.nextToken ();
+      String value_str = tokenizer.nextToken ();
+      if(name_str != null && value_str != null) {
+	command_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, name_str);
+	// If the value is true or false we add it as an attribute
+	if(value_str.equalsIgnoreCase (StaticStrings.TRUE_STR) || value_str.equalsIgnoreCase (StaticStrings.FALSE_STR)) {
+	  command_element.setAttribute (StaticStrings.VALUE_ATTRIBUTE, value_str.toLowerCase ());
+	}
+	// Otherwise it gets added as a text node
+	else {
+	  // Ready the value str (which can contain all sorts of funky characters) for writing as a DOM value
+	  value_str = Codec.transform (value_str, Codec.GREENSTONE_TO_DOM);
+	  XMLTools.setValue (command_element, value_str);
+	}
+      }
+      else {
+	command_element = null;
+      }
+      name_str = null;
+      value_str = null;
+    }
+    catch (Exception exception) {
+      DebugStream.printStackTrace (exception);
+      command_element = null;
+    }
+    return command_element;
+  }
+    
+  static private Element parseIndex (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      String command = tokenizer.nextToken ();
+      command_element = document.createElement (StaticStrings.INDEXES_ELEMENT);
+      command_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, (command.equals (StaticStrings.INDEX_STR) ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+      command = null;
+      if(!tokenizer.hasMoreTokens ()) {
+                
+	// there are no indexes
+	command_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+	command_element.setAttribute (StaticStrings.MGPP_ATTRIBUTE, StaticStrings.FALSE_STR); // for now
+	tokenizer = null;
+	return command_element;
+      }
+            
+      while(tokenizer.hasMoreTokens ()) {
+	Element index_element = document.createElement (StaticStrings.INDEX_ELEMENT);
+	String index_str = tokenizer.nextToken ();
+	// There are two types of index we have to consider. MG versions use "level:source,source" while MGPP versions use "source,source source" 
+	if(index_str.indexOf (StaticStrings.COLON_CHARACTER) != -1) {
+	  index_element.setAttribute (StaticStrings.LEVEL_ATTRIBUTE, index_str.substring (0, index_str.indexOf (StaticStrings.COLON_CHARACTER)));
+	  index_str = index_str.substring (index_str.indexOf (StaticStrings.COLON_CHARACTER) + 1);
+	  command_element.setAttribute (StaticStrings.MGPP_ATTRIBUTE, StaticStrings.FALSE_STR);
+	}
+	else {
+	  command_element.setAttribute (StaticStrings.MGPP_ATTRIBUTE, StaticStrings.TRUE_STR);
+	}
+	StringTokenizer content_tokenizer = new StringTokenizer (index_str, StaticStrings.COMMA_CHARACTER);
+	while(content_tokenizer.hasMoreTokens ()) {
+	  Element content_element = document.createElement (StaticStrings.CONTENT_ELEMENT);
+	  String content_str = content_tokenizer.nextToken ();
+	  // Since the contents of indexes have to be certain keywords, or metadata elements, if the content isn't a keyword and doesn't yet have a namespace, append the extracted metadata namespace.
+	  if(content_str.indexOf (StaticStrings.NS_SEP) == -1) {
+	    if(content_str.equals (StaticStrings.TEXT_STR) || content_str.equals (StaticStrings.ALLFIELDS_STR) || content_str.equals(StaticStrings.METADATA_STR)) {
+	      // Our special strings are OK.
+	    }
+	    else {
+	      content_str = StaticStrings.EXTRACTED_NAMESPACE + content_str;
+	    }
+	  }
+	  content_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, content_str);
+	  index_element.appendChild (content_element);
+	  content_element = null;
+	}
+	content_tokenizer = null;
+	index_str = null;
+	command_element.appendChild (index_element);
+	index_element = null;
+      }
+      tokenizer = null;
+    }
+    catch (Exception exception) {
+      exception.printStackTrace ();
+    }
+    return command_element;
+  }
+    
+  static private Element parseIndexDefault (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      if(tokenizer.countTokens () >= 2) {
+	command_element = document.createElement (StaticStrings.INDEX_DEFAULT_ELEMENT);
+	command_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, ((tokenizer.nextToken ()).equals (StaticStrings.INDEX_DEFAULT_STR) ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	String index_str = tokenizer.nextToken ();
+	String level="";
+	if (index_str.indexOf (StaticStrings.COLON_CHARACTER) !=-1) {
+	  level =  index_str.substring (0, index_str.indexOf (StaticStrings.COLON_CHARACTER));
+	}
+                
+	command_element.setAttribute (StaticStrings.LEVEL_ATTRIBUTE,level);
+                
+	String content_str = index_str;
+                
+	if (index_str.indexOf (StaticStrings.COLON_CHARACTER) !=-1) {
+	  content_str = index_str.substring (index_str.indexOf (StaticStrings.COLON_CHARACTER) + 1);
+	}
+                
+	StringTokenizer content_tokenizer = new StringTokenizer (content_str, StaticStrings.COMMA_CHARACTER);
+	while(content_tokenizer.hasMoreTokens ()) {
+	  Element content_element = document.createElement (StaticStrings.CONTENT_ELEMENT);
+	  content_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, content_tokenizer.nextToken ());
+	  command_element.appendChild (content_element);
+	  content_element = null;
+	}
+	content_tokenizer = null;
+	content_str = null;
+	content_str = null;
+	index_str = null;
+      }
+      tokenizer = null;
+    }
+    catch (Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private Element parseLanguage (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      tokenizer.nextToken ();
+      if(tokenizer.hasMoreTokens ()) {
+	command_element = document.createElement (StaticStrings.LANGUAGES_ELEMENT);
+	while(tokenizer.hasMoreTokens ()) {
+	  Element language_element = document.createElement (StaticStrings.LANGUAGE_ELEMENT);
+	  language_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, tokenizer.nextToken ());
+	  command_element.appendChild (language_element);
+	  language_element = null;
+	}
+      }
+      tokenizer = null;
+    }
+    catch (Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private Element parseLanguageDefault (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      if(tokenizer.countTokens () >= 2) {
+	command_element = document.createElement (StaticStrings.LANGUAGE_DEFAULT_ELEMENT);
+	tokenizer.nextToken ();
+	String default_language_str = tokenizer.nextToken ();
+	command_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, default_language_str);
+	command_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+	default_language_str = null;
+      }
+      tokenizer = null;
+    }
+    catch (Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private Element parseLanguageMetadata (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      if(tokenizer.countTokens () >= 2) {
+	command_element = document.createElement (StaticStrings.LANGUAGE_METADATA_ELEMENT);
+	tokenizer.nextToken ();
+	String language_metadata_str = tokenizer.nextToken ();
+	if (language_metadata_str.indexOf (StaticStrings.NS_SEP) == -1) {
+	  language_metadata_str = StaticStrings.EXTRACTED_NAMESPACE + language_metadata_str;
+	}
+	command_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, language_metadata_str);
+	command_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+	language_metadata_str = null;
+      }
+      tokenizer = null;
+            
+    }
+    catch (Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private Element parseIndexOptions (String command_str, Document document, String type, boolean assigned) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      // First token is command type
+      String command = tokenizer.nextToken ();
+      if(tokenizer.hasMoreTokens ()) {
+	command_element = document.createElement (StaticStrings.INDEXOPTIONS_ELEMENT);
+	command_element.setAttribute (StaticStrings.NAME_ATTRIBUTE,type);
+	command_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	while(tokenizer.hasMoreTokens ()) {
+	  Element option_element = document.createElement (StaticStrings.INDEXOPTION_ELEMENT);
+	  option_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, tokenizer.nextToken ());
+	  command_element.appendChild (option_element);
+	  option_element = null;
+	}
+      }
+      command = null;
+    }
+    catch(Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private Element parseIndexOptionDefault (String command_str, Document document, String type, boolean assigned) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      // First token is command type
+      String command = tokenizer.nextToken ();
+      if(tokenizer.hasMoreTokens ()) {
+	command_element = document.createElement (StaticStrings.INDEXOPTION_DEFAULT_ELEMENT);
+	command_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR)); // is it commented out or not?
+	command_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, type);
+	command_element.setAttribute (StaticStrings.VALUE_ATTRIBUTE, tokenizer.nextToken ());
+      }
+            
+      tokenizer = null;
+    }
+    catch (Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private Element parseMetadata (CommandTokenizer tokenizer, Document document) {
+    Element command_element = null;
+    try {
+      command_element = document.createElement (StaticStrings.COLLECTIONMETADATA_ELEMENT);
+      String name_str = tokenizer.nextToken ();
+      String value_str = tokenizer.nextToken ();
+      if(name_str != null && value_str != null) {
+	String language_str = Configuration.getLanguage ();
+	// Check if the value string is actually a language string
+	if(value_str.startsWith (StaticStrings.LBRACKET_CHARACTER) && value_str.endsWith (StaticStrings.RBRACKET_CHARACTER)) {
+	  language_str = value_str.substring (value_str.indexOf (StaticStrings.LANGUAGE_ARGUMENT) + 2, value_str.length () - 1);
+	  value_str = tokenizer.nextToken ();
+	}
+	if(value_str != null) {
+	  // Ready the value str (which can contain all sorts of funky characters) for writing as a DOM value
+	  value_str = Codec.transform (value_str, Codec.GREENSTONE_TO_DOM);
+	  command_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, name_str);
+	  command_element.setAttribute (StaticStrings.LANGUAGE_ATTRIBUTE, language_str);
+	  command_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+	  XMLTools.setValue (command_element, value_str);
+	}
+	else {
+	  command_element = null;
+	}
+	language_str = null;
+      }
+      else {
+	command_element = null;
+      }
+      name_str = null;
+      value_str = null;
+    }
+    catch (Exception exception) {
+      DebugStream.printStackTrace (exception);
+      command_element = null;
+    }
+    return command_element;
+  }
+    
+  static private Element parseMetadataSpecial (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      if(tokenizer.countTokens () >= 2) {
+	String name_str = tokenizer.nextToken ();
+	String value_str = tokenizer.nextToken ();
+	if (name_str.equals (StaticStrings.COLLECTIONMETADATA_CREATOR_STR)) {
+	  command_element = document.createElement (StaticStrings.COLLECTIONMETADATA_CREATOR_ELEMENT);
+	}
+	else if(name_str.equals (StaticStrings.COLLECTIONMETADATA_MAINTAINER_STR)) {
+	  command_element = document.createElement (StaticStrings.COLLECTIONMETADATA_MAINTAINER_ELEMENT);
+	}
+	else if(name_str.equals (StaticStrings.COLLECTIONMETADATA_PUBLIC_STR)) {
+	  command_element = document.createElement (StaticStrings.COLLECTIONMETADATA_PUBLIC_ELEMENT);
+	}
+	else if (name_str.equals (StaticStrings.BUILDTYPE_STR)) {
+	  command_element = document.createElement (StaticStrings.BUILDTYPE_ELEMENT);
+	}
+	else if (name_str.equals (StaticStrings.DATABASETYPE_STR)) {
+	  command_element = document.createElement (StaticStrings.DATABASETYPE_ELEMENT);
+	}
+	if(command_element != null) {
+	  command_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, name_str);
+	  command_element.setAttribute (StaticStrings.LANGUAGE_ATTRIBUTE, StaticStrings.ENGLISH_LANGUAGE_STR);
+	  command_element.setAttribute (StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
+	  command_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+	  if(value_str.startsWith (StaticStrings.SPEECH_CHARACTER) && value_str.endsWith (StaticStrings.SPEECH_CHARACTER)) {
+	    value_str = value_str.substring (1, value_str.length () - 1);
+	  }
+	  XMLTools.setValue (command_element, value_str);
+	}
+	value_str = null;
+	name_str = null;
+      }
+      tokenizer = null;
+    }
+    catch (Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private Element parsePlugin (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      CommandTokenizer tokenizer = new CommandTokenizer (command_str);
+      // Check the token count. The token count from a command tokenizer isn't guarenteed to be correct, but it does give the maximum number of available tokens according to the underlying StringTokenizer (some of which may actually be append together by the CommandTokenizer as being a single argument).
+      if(tokenizer.countTokens () >= 2) {
+	command_element = document.createElement (StaticStrings.PLUGIN_ELEMENT);
+	// First token is plugin
+	tokenizer.nextToken ();
+	// The next token is the type
+	String type = tokenizer.nextToken ();
+	type = Utility.ensureNewPluginName(type);
+	command_element.setAttribute (StaticStrings.TYPE_ATTRIBUTE, type);
+	// Now we parse out the remaining arguments into a hashmapping from name to value
+	HashMap arguments = parseArguments (tokenizer);
+	// also watch out for the deprecated -use_metadata_files option to RecPlug and remove it
+	Iterator names = arguments.keySet ().iterator ();
+	while(names.hasNext ()) {
+	  String name = (String) names.next ();
+	  String value = (String) arguments.get (name); // Can be null
+                    
+	  if(type.equals (StaticStrings.RECPLUG_STR) && name.substring (1).equals (StaticStrings.USE_METADATA_FILES_ARGUMENT)) {
+	    continue; // ignore this option
+	  }
+	  Element option_element = document.createElement (StaticStrings.OPTION_ELEMENT);
+	  option_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, name.substring (1));
+	  option_element.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+	  if(value != null) {
+	    // Remove any speech marks appended in strings containing whitespace
+	    if(value.startsWith (StaticStrings.SPEECH_CHARACTER) && value.endsWith (StaticStrings.SPEECH_CHARACTER)) {
+	      value = value.substring (1, value.length () - 1);
+	    }
+	    XMLTools.setValue (option_element, value);
+	  }
+	  command_element.appendChild (option_element);
+	  option_element = null;
+	  name = null;
+	  value = null;
+	}
+                
+	type = null;
+	names = null;
+	arguments = null;
+      }
+      tokenizer = null;
+    }
+    catch(Exception exception) {
+      // This catch clause had been left empty.  If this is deliberate then
+      // we should have a comment here explaining why there is no need to
+      // print anything out.  Am assuming this is mistake for now, and
+      // have added in a call to printStackTrace()
+      System.err.println("Malformed plugin statement");
+      exception.printStackTrace();
+    }
+    return command_element;
+  }
+    
+  /* search types are now handled as formats - leave this here to convert in case we have an old config file */
+  static private Element parseSearchType (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      // First token is command type (searchtype)
+      tokenizer.nextToken ();
+      if(tokenizer.hasMoreTokens ()) {
+	command_element = document.createElement (StaticStrings.FORMAT_ELEMENT);
+	command_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, "SearchTypes");
+	String value = tokenizer.nextToken ();
+	while(tokenizer.hasMoreTokens ()) {
+	  value += ","+tokenizer.nextToken ();
+	}
+	value = Codec.transform (value, Codec.GREENSTONE_TO_DOM);
+	XMLTools.setValue (command_element, value);
+      }
+    }
+    catch(Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private Element parseSubCollection (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      CommandTokenizer tokenizer = new CommandTokenizer (command_str);
+      if(tokenizer.countTokens () >= 3) {
+	command_element = document.createElement (StaticStrings.SUBCOLLECTION_ELEMENT);
+	// First token is command type
+	tokenizer.nextToken ();
+	// Then subcollection identifier
+	command_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, tokenizer.nextToken ());
+	// Then finally the pattern used to build the subcollection partition
+	String full_pattern_str = tokenizer.nextToken ();
+	// Set inclusion/exclusion flag and remove any exclamation mark
+	boolean exclusion = full_pattern_str.startsWith (StaticStrings.EXCLAMATION_CHARACTER);
+	if (exclusion) {
+	  full_pattern_str = full_pattern_str.substring (1, full_pattern_str.length ());
+	  command_element.setAttribute (StaticStrings.TYPE_ATTRIBUTE, StaticStrings.EXCLUDE_STR);
+	}
+	else {
+	  command_element.setAttribute (StaticStrings.TYPE_ATTRIBUTE, StaticStrings.INCLUDE_STR);
+	}
+
+	// Let's make sure it is a valid Greenstone configuration line
+	String[] results = full_pattern_str.split("\\" + StaticStrings.SEPARATOR_CHARACTER, 3);
+
+	if (results.length >= 2) {
+	  String content_str = results[0];
+	  // Since the contents of indexes have to be certain keywords, or metadata elements, if the content isn't a keyword and doesn't yet have a namespace, append the extracted metadata namespace.
+	  if (!content_str.equals (StaticStrings.FILENAME_STR) && content_str.indexOf (StaticStrings.NS_SEP) == -1) {
+	    content_str = StaticStrings.EXTRACTED_NAMESPACE + content_str;
+	  }
+	  command_element.setAttribute (StaticStrings.CONTENT_ATTRIBUTE, content_str);
+	  XMLTools.setValue (command_element, results[1]);
+	  if (results.length >= 3) {
+	    command_element.setAttribute (StaticStrings.OPTIONS_ATTRIBUTE, results[2]);
+	  }
+	}
+      }
+    }
+    catch(Exception exception) {
+      exception.printStackTrace ();
+    }
+    return command_element;
+  }
+    
+  static private Element parseSubCollectionDefaultIndex (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      if(tokenizer.countTokens () == 2) {
+	command_element = document.createElement (StaticStrings.SUBCOLLECTION_DEFAULT_INDEX_ELEMENT);
+	tokenizer.nextToken ();
+	//command_element.setAttribute(CONTENT_ATTRIBUTE, tokenizer.nextToken());
+	String content_str = tokenizer.nextToken ();
+	StringTokenizer content_tokenizer = new StringTokenizer (content_str, StaticStrings.COMMA_CHARACTER);
+	while(content_tokenizer.hasMoreTokens ()) {
+	  Element content_element = document.createElement (StaticStrings.CONTENT_ELEMENT);
+	  content_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, content_tokenizer.nextToken ());
+	  command_element.appendChild (content_element);
+	  content_element = null;
+	}
+	content_tokenizer = null;
+	content_str = null;
+      }
+      tokenizer = null;
+    }
+    catch(Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private Element parseSubCollectionIndex (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      tokenizer.nextToken ();
+      if(tokenizer.hasMoreTokens ()) {
+	command_element = document.createElement (StaticStrings.SUBCOLLECTION_INDEXES_ELEMENT);
+      }
+      while(tokenizer.hasMoreTokens ()) {
+	Element subcollectionindex_element = document.createElement (StaticStrings.INDEX_ELEMENT);
+	//command_element.setAttribute(CONTENT_ATTRIBUTE, tokenizer.nextToken());
+	String content_str = tokenizer.nextToken ();
+	StringTokenizer content_tokenizer = new StringTokenizer (content_str, StaticStrings.COMMA_CHARACTER);
+	while(content_tokenizer.hasMoreTokens ()) {
+	  Element content_element = document.createElement (StaticStrings.CONTENT_ELEMENT);
+	  content_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, content_tokenizer.nextToken ());
+	  subcollectionindex_element.appendChild (content_element);
+	  content_element = null;
+	}
+	content_tokenizer = null;
+	content_str = null;
+	command_element.appendChild (subcollectionindex_element);
+	subcollectionindex_element = null;
+      }
+      tokenizer = null;
+    }
+    catch (Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private Element parseSuperCollection (String command_str, Document document) {
+    Element command_element = null;
+    try {
+      StringTokenizer tokenizer = new StringTokenizer (command_str);
+      if(tokenizer.countTokens () >= 3) {
+	command_element = document.createElement (StaticStrings.SUPERCOLLECTION_ELEMENT);
+	tokenizer.nextToken ();
+	while(tokenizer.hasMoreTokens ()) {
+	  Element collection_element = document.createElement (StaticStrings.COLLECTION_ELEMENT);
+	  collection_element.setAttribute (StaticStrings.NAME_ATTRIBUTE, tokenizer.nextToken ());
+	  command_element.appendChild (collection_element);
+	  collection_element = null;
+	}
+      }
+      tokenizer = null;
+    }
+    catch(Exception exception) {
+    }
+    return command_element;
+  }
+    
+  static private String pluginToString (Element command_element) {
+    if(command_element.getAttribute (StaticStrings.SEPARATOR_ATTRIBUTE).equals (StaticStrings.TRUE_STR)) { 
+      return "";
+    }
+    StringBuffer text = new StringBuffer (StaticStrings.PLUGIN_STR);
+    text.append (StaticStrings.TAB_CHARACTER);
+    text.append (command_element.getAttribute (StaticStrings.TYPE_ATTRIBUTE));
+    // Retrieve, and output, the arguments
+    NodeList option_elements = command_element.getElementsByTagName (StaticStrings.OPTION_ELEMENT);
+    int option_elements_length = option_elements.getLength ();
+    if(option_elements_length > 0) {
+      for(int j = 0; j < option_elements_length; j++) {
+	Element option_element = (Element) option_elements.item (j);
+	if(option_element.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.TRUE_STR)) {
+	  text.append (StaticStrings.SPACE_CHARACTER);
+	  text.append (StaticStrings.MINUS_CHARACTER);
+	  text.append (option_element.getAttribute (StaticStrings.NAME_ATTRIBUTE));
+	  String value_str = XMLTools.getValue (option_element);
+	  if (value_str.length () > 0) {
+	    text.append (StaticStrings.SPACE_CHARACTER);
+	    if(value_str.indexOf (StaticStrings.SPACE_CHARACTER) != -1) {
+	      // enclose in quotes
+	      text.append(StaticStrings.SPEECH_CHARACTER);
+	      text.append(value_str);
+	      text.append(StaticStrings.SPEECH_CHARACTER);
+	    } else {
+		    
+	      text.append(value_str);
+	    }
+	  }
+	      
+	  value_str = null;
+	}
+	option_element = null;
+      }
+    }
+    option_elements = null;
+	
+    return text.toString ();
+  }
+
+  static public String generateStringVersion(Document document) {
+
+    StringBuffer collect_cfg_string_buffer = new StringBuffer ();
+    NodeList command_elements = document.getDocumentElement ().getChildNodes ();
+    boolean just_wrote_blank_line = false;  // Prevent two or more blank lines in a row
+    for (int i = 0; i < command_elements.getLength (); i++) {
+      Node command_node = command_elements.item (i);
+      if (!(command_node instanceof Element)) {
+	// We're only interested in Elements
+	continue;
+      }
+      Element command_element = (Element) command_node;
+            
+      // Handle NewLine elements (blank lines)
+      if (command_element.getNodeName ().equals (CollectionConfiguration.NEWLINE_ELEMENT) && !just_wrote_blank_line) {
+	collect_cfg_string_buffer.append ("\n");
+	just_wrote_blank_line = true;
+      }
+            
+      // Anything else we write to file, but only if it has been assigned, except for index and level commands
+      // (which just get commented out if unassigned -- a side effect of MG & MGPP compatibility)
+      else if (!command_element.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.FALSE_STR) || command_element.getNodeName ().equals (StaticStrings.INDEXES_ELEMENT) || command_element.getNodeName ().equals (StaticStrings.INDEX_DEFAULT_ELEMENT) || command_element.getNodeName ().equals (StaticStrings.INDEXOPTIONS_ELEMENT) || command_element.getNodeName ().equals (StaticStrings.INDEXOPTION_DEFAULT_ELEMENT)) {
+	String command = toString(command_element);
+                
+	if (command != null && command.length ()> 0 ) {
+	  collect_cfg_string_buffer.append (command + "\n");
+	  just_wrote_blank_line = false;
+	}
+      }
+    }
+        
+    return collect_cfg_string_buffer.toString ();
+  }
+
+   
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionConfigXMLReadWrite.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionConfigXMLReadWrite.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionConfigXMLReadWrite.java	(revision 31635)
@@ -0,0 +1,2845 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Methods to read collectionConfig.xml files into internal XML form, and write 
+ * them back out again.
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+public class CollectionConfigXMLReadWrite
+{
+
+	static final private String PLUGOUT_ELEMENT = "plugout";//used by building flax collections
+
+	// a list of all known top level elements
+  static final private String known_element_names_array[] = { StaticStrings.SECURITY_STR, StaticStrings.METADATALIST_STR, StaticStrings.DISPLAYITEMLIST_STR, StaticStrings.FORMAT_STR, StaticStrings.SEARCH_STR, StaticStrings.INFODB_STR, StaticStrings.BROWSE_STR, StaticStrings.IMPORT_STR, 
+  /*StaticStrings.IMPORT_OPTION_STR, StaticStrings.BUILD_OPTION_STR,*/
+  StaticStrings.DISPLAY_STR, StaticStrings.REPLACELISTREF_STR, StaticStrings.REPLACELIST_STR, StaticStrings.SERVICE_RACK_LIST_ELEMENT };
+	static final private Set known_element_names = new HashSet(Arrays.asList(known_element_names_array));
+
+	/**
+	 * *************************************************************************
+	 * ******************************* The code in this file is used for
+	 * greenstone 3 collection configuration, i.e., read ColletionConfig.xml
+	 * into the internal DOM tree, and convert the internal DOM tree back to
+	 * CollectionConfig.xml.
+	 * 
+	 * Methods named 'doXXXX' are for convert collectionConfig.xml into the
+	 * internal configuration xml structure; Methods named 'convertXXXX' are for
+	 * convert the internal configuration xml structure back to
+	 * collectionConfig.xml.
+	 ************************************************************************************************************ */
+
+	/**
+	 * Arguments: metadataListNode->the 'displayItemList' element in
+	 * collectionConfig.xml name_value->the value of the 'name' attribute of
+	 * 'index' element; att_value->the value of the 'name' attribute of
+	 * 'displayItem' element return: an ArrayList of the contructed
+	 * 'CollectionMetadata' elements
+	 */
+	static private ArrayList doDisplayItemList(Document to, Node displayListNode, String att_value, String name_value)
+	{
+		Element toElement = to.getDocumentElement();
+		ArrayList display_item_list = new ArrayList();
+		ArrayList item_list = XMLTools.getNamedElementList((Element) displayListNode, StaticStrings.DISPLAYITEM_STR, StaticStrings.NAME_ATTRIBUTE, att_value);
+		if (item_list == null)
+		{
+			return null;
+		}
+
+		for (int i = 0; i < item_list.size(); i++)
+		{
+			Element item = (Element) item_list.get(i);
+			String text = XMLTools.getNodeText(item);			
+
+			//If there is nothing to display, don't bother creating the element
+			// Either lang or key should be set. If key set, no textnode.
+			// if lang set, should have textnode. Check if it's meaningful
+			if (item.hasAttribute(StaticStrings.LANG_STR) && text.equals(""))
+			{
+				continue;
+			}		
+
+			// get the value in 'lang=langcode' or 'key=keyname'
+			// We can have either a key attribute (with optional dictionary attribute, else dictionary is implicit)
+			// OR we can have a lang attr, with the nodetext containing the actual display string.
+			String lang = item.getAttribute(StaticStrings.LANG_STR);
+			String key = item.getAttribute(StaticStrings.KEY_ATTRIBUTE);
+
+			Element e = to.createElement(StaticStrings.COLLECTIONMETADATA_ELEMENT);
+			e.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+			e.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_value);
+
+			if(!lang.equals("")) {
+			    e.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, lang);
+			    text = text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
+			    XMLTools.setNodeText(e, text);
+			    
+			}
+
+			if(!key.equals("")) {
+			    e.setAttribute(StaticStrings.KEY_ATTRIBUTE, key);
+			    String dictionary = item.getAttribute(StaticStrings.DICTIONARY_ATTRIBUTE);
+			    if(!dictionary.equals("")) {
+				e.setAttribute(StaticStrings.DICTIONARY_ATTRIBUTE, dictionary);
+			    }
+			}
+
+			display_item_list.add(e);
+		}
+		return display_item_list;
+	}
+
+	static private ArrayList doMetadataList(Document to, Node metadataListNode, String ele_name, String att_value)
+	{
+		Element toElement = to.getDocumentElement();
+		ArrayList metadata_list = new ArrayList();
+
+		ArrayList item_list = XMLTools.getNamedElementList((Element) metadataListNode, StaticStrings.METADATA_STR, StaticStrings.NAME_ATTRIBUTE, att_value);
+		if (item_list == null)
+		{
+			return null;
+		}
+
+		for (int i = 0; i < item_list.size(); i++)
+		{
+			Element item = (Element) item_list.get(i);
+			String text = XMLTools.getNodeText(item);
+
+			//If there is nothing to display, don't bother creating the element
+			if (text.equals(""))
+			{
+				continue;
+			}
+			//get the value in 'lang=value'
+			String lang = item.getAttribute(StaticStrings.LANG_STR);
+
+			Element element = to.createElement(ele_name);
+			element.setAttribute(StaticStrings.NAME_ATTRIBUTE, att_value);
+			element.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, lang);
+			element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+			element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
+			XMLTools.setNodeText(element, text);
+
+			metadata_list.add(element);
+		}
+		return metadata_list;
+	}
+
+	// 'to' is the internal structure
+	static private void doMGIndexes(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		Element indexes_element = to.createElement(StaticStrings.INDEXES_ELEMENT);//<Indexes>
+		indexes_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+		indexes_element.setAttribute(StaticStrings.MGPP_ATTRIBUTE, StaticStrings.FALSE_STR);
+
+		NodeList index_children = ((Element) searchNode).getElementsByTagName(StaticStrings.INDEX_LOW_STR);//index
+		int num_nodes = index_children.getLength();
+
+		for (int i = 0; i < num_nodes; i++)
+		{
+			Element e = (Element) index_children.item(i);
+			String index_str = e.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			String index_str_display = index_str;//used for creating collectionmetadata for this index
+			Element index_element = to.createElement(StaticStrings.INDEX_ELEMENT);//<Index>
+
+			if (index_str.indexOf(StaticStrings.COLON_CHARACTER) == -1)
+			{
+				// It doesn't contain ':' character
+				System.err.println("Something is wrong! the index should be level:source tuplets.");
+				// assume document level
+				index_element.setAttribute(StaticStrings.LEVEL_ATTRIBUTE, StaticStrings.DOCUMENT_STR);
+			}
+			else
+			{
+				// Handling 'index' element
+				index_element.setAttribute(StaticStrings.LEVEL_ATTRIBUTE, index_str.substring(0, index_str.indexOf(StaticStrings.COLON_CHARACTER)));
+
+				index_str = index_str.substring(index_str.indexOf(StaticStrings.COLON_CHARACTER) + 1);
+			}
+			//Each index may have a list of comma-separated strings.
+			//split them into 'content' elements in the internal structure
+			StringTokenizer content_tokenizer = new StringTokenizer(index_str, StaticStrings.COMMA_CHARACTER);
+			//index_str = "";
+			while (content_tokenizer.hasMoreTokens())
+			{
+				// Replace index_str to be qualified name, eg. dc.Subject and keywords insread of dc.Subject.
+
+				Element content_element = to.createElement(StaticStrings.CONTENT_ELEMENT);
+				String content_str = content_tokenizer.nextToken();
+				// Since the contents of indexes have to be certain keywords, or metadata elements,
+				//if the content isn't a keyword and doesn't yet have a namespace, append the extracted metadata namespace.
+				if (content_str.indexOf(StaticStrings.NS_SEP) == -1 && !(content_str.equals(StaticStrings.TEXT_STR)))
+				{
+					content_str = StaticStrings.EXTRACTED_NAMESPACE + content_str;
+
+				}
+
+				content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, content_str);
+				index_element.appendChild(content_element);
+				content_element = null;
+			} // while ends
+
+			indexes_element.appendChild(index_element);
+
+			// Handling 'displayItem' elements and Constructing 'collectionmetadata' elements
+			// Use the fully qualified index names
+			ArrayList collectionmetadata_list = doDisplayItemList(to, e, StaticStrings.NAME_ATTRIBUTE, index_str_display);
+			appendArrayList(toElement, collectionmetadata_list);
+		} //for loop ends
+		appendProperly(toElement, indexes_element);
+
+		//***//
+		// create another set of <indexes> which will be used when user switches to MGPP/LUCENE
+		// i.e. we build a default index set for a start
+
+		String[] index_strs = { StaticStrings.TEXT_STR, StaticStrings.EXTRACTED_NAMESPACE + StaticStrings.TITLE_ELEMENT, StaticStrings.EXTRACTED_NAMESPACE + StaticStrings.SOURCE_ELEMENT };
+
+		Element mgpp_indexes = to.createElement(StaticStrings.INDEXES_ELEMENT);//<Indexes>
+		mgpp_indexes.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+		mgpp_indexes.setAttribute(StaticStrings.MGPP_ATTRIBUTE, StaticStrings.TRUE_STR);
+		for (int i = 0; i < index_strs.length; i++)
+		{
+			Element index_element = to.createElement(StaticStrings.INDEX_ELEMENT);//<Index>
+			Element content_element = to.createElement(StaticStrings.CONTENT_ELEMENT);
+			content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, index_strs[i]);
+			index_element.appendChild(content_element);
+			mgpp_indexes.appendChild(index_element);
+
+			// Contructing 'collectionmetadata' elements for 'mgpp' indexes
+			Element collectionmetadata = to.createElement(StaticStrings.COLLECTIONMETADATA_ELEMENT);
+			collectionmetadata.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+			collectionmetadata.setAttribute(StaticStrings.NAME_ATTRIBUTE, index_strs[i]);
+			collectionmetadata.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, StaticStrings.ENGLISH_LANGUAGE_STR);
+			if (index_strs[i].indexOf(StaticStrings.NS_SEP) != -1)
+			{
+				index_strs[i] = index_strs[i].substring(index_strs[i].indexOf(StaticStrings.NS_SEP) + 1);
+			}
+			XMLTools.setNodeText(collectionmetadata, index_strs[i]);
+
+			appendProperly(toElement, collectionmetadata);
+
+		}
+		appendProperly(toElement, mgpp_indexes);
+	}
+
+	//This is actually doing indexes for both mgpp and lucene
+	static private void doMGPPIndexes(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		Element indexes_element = to.createElement(StaticStrings.INDEXES_ELEMENT);//<Indexes>
+		indexes_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+		indexes_element.setAttribute(StaticStrings.MGPP_ATTRIBUTE, StaticStrings.TRUE_STR);
+
+		NodeList index_children = ((Element) searchNode).getElementsByTagName(StaticStrings.INDEX_LOW_STR);//index
+		int num_nodes = index_children.getLength();
+
+		for (int i = 0; i < num_nodes; i++)
+		{
+
+			Element index_element = to.createElement(StaticStrings.INDEX_ELEMENT);//<Index>
+			Element e = (Element) index_children.item(i);
+			String index_str = e.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			String index_str_display = index_str;//for creating collectionmetadata for this index 
+
+
+			String options_str = "";
+			NodeList option_children = e.getElementsByTagName(StaticStrings.OPTION_STR);
+			if(option_children != null) {
+			   for (int j = 0; j < option_children.getLength(); j++) {
+				Element el = (Element) option_children.item(j);
+
+				String name_str = el.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+				options_str = options_str + ((name_str.equals("")) ? "" : (" " + name_str));
+
+				String value_str = el.getAttribute(StaticStrings.VALUE_ATTRIBUTE);
+				options_str = options_str + ((name_str.equals("")) ? "" : (" " + value_str));
+
+				if (name_str.equals("") && !value_str.equals(""))
+				{
+					continue; //invalid condition
+				}
+
+				Element option_element = null;
+				option_element = to.createElement(StaticStrings.OPTION_ELEMENT);
+				option_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+
+				if (!name_str.equals(""))
+				{
+					option_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+				}
+				if (value_str != null && !value_str.equals(""))
+				{
+					XMLTools.setNodeText(option_element, value_str);
+				}
+				index_element.appendChild(option_element);
+			   }
+			}
+
+			// Handling 'index' element
+			// Double check to make sure it's not colon separated style index.
+			if (index_str.indexOf(StaticStrings.COLON_CHARACTER) != -1)
+			{
+				System.err.println("Something is wrong! the index should NOT be level:source tuplets style.");
+				index_str = index_str.substring(index_str.indexOf(StaticStrings.COLON_CHARACTER) + 1);
+
+			}
+			//Each index may have a list of comma-separated strings.
+			//split them into 'content' elements in the internal structure
+			StringTokenizer content_tokenizer = new StringTokenizer(index_str, StaticStrings.COMMA_CHARACTER);
+			while (content_tokenizer.hasMoreTokens())
+			{
+				// Replace index_str to be qualified name, eg. dc.Subject and keywords insread of dc.Subject.             
+
+				Element content_element = to.createElement(StaticStrings.CONTENT_ELEMENT);
+				String content_str = content_tokenizer.nextToken();
+				// Since the contents of indexes have to be certain keywords, or metadata elements, if the content isn't a keyword and doesn't yet have a namespace, append the extracted metadata namespace.
+				if (content_str.indexOf(StaticStrings.NS_SEP) == -1 && !(content_str.equals(StaticStrings.TEXT_STR) || content_str.equals(StaticStrings.ALLFIELDS_STR)))
+				{
+					content_str = StaticStrings.EXTRACTED_NAMESPACE + content_str;
+				}
+				content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, content_str);
+				index_element.appendChild(content_element);
+				content_element = null;
+			} //while ends
+
+			indexes_element.appendChild(index_element);
+
+			index_element = null;
+
+			// Handling 'displayItem' element of this 'index' element
+			// 'e' is the parent element 'index' of 'displayItem' element
+			ArrayList collectionmetadata_list = doDisplayItemList(to, e, StaticStrings.NAME_ATTRIBUTE, index_str_display);
+			appendArrayList(toElement, collectionmetadata_list);
+
+		} // for loop ends
+		toElement.appendChild(indexes_element);
+
+		// create another set of <indexes> which will be used when user switches to MG
+		// i.e. we build a default index set for a start
+		Element mg_indexes = to.createElement(StaticStrings.INDEXES_ELEMENT);//<Indexes>
+		mg_indexes.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+		mg_indexes.setAttribute(StaticStrings.MGPP_ATTRIBUTE, StaticStrings.FALSE_STR);
+
+		//put the namespace '.ex' as prefix to the indexes
+		String[] index_strs = { StaticStrings.TEXT_STR, StaticStrings.EXTRACTED_NAMESPACE + StaticStrings.TITLE_ELEMENT, StaticStrings.EXTRACTED_NAMESPACE + StaticStrings.SOURCE_ELEMENT };
+		for (int i = 0; i < index_strs.length; i++)
+		{
+			Element index_element = to.createElement(StaticStrings.INDEX_ELEMENT);//<Index>
+			index_element.setAttribute(StaticStrings.LEVEL_ATTRIBUTE, StaticStrings.DOCUMENT_STR);
+			Element content_element = to.createElement(StaticStrings.CONTENT_ELEMENT);
+			content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, index_strs[i]);
+			index_element.appendChild(content_element);
+
+			mg_indexes.appendChild(index_element);
+
+			// Contructing 'collectionmetadata' elements for 'mg' indexes
+			Element collectionmetadata = to.createElement(StaticStrings.COLLECTIONMETADATA_ELEMENT);
+			collectionmetadata.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+			String temp = StaticStrings.DOCUMENT_STR.concat(StaticStrings.COLON_CHARACTER).concat(index_strs[i]);
+			collectionmetadata.setAttribute(StaticStrings.NAME_ATTRIBUTE, temp);
+			collectionmetadata.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, StaticStrings.ENGLISH_LANGUAGE_STR);
+			if (index_strs[i].indexOf(StaticStrings.NS_SEP) != -1)
+			{
+				index_strs[i] = index_strs[i].substring(index_strs[i].indexOf(StaticStrings.NS_SEP) + 1);
+			}
+			XMLTools.setNodeText(collectionmetadata, index_strs[i]);
+
+			appendProperly(toElement, collectionmetadata);
+
+		}
+		toElement.appendChild(mg_indexes);
+
+	}
+
+  //This processes <sort> (lucene), or <sort> and <facet> (solr) subelements of <search> also <defaultSort>
+  // Note that GLI at present does not allow the user to set or modify these.
+  // This function, and its inverse convertSolrFacetsAndSorts, are only here to preserve such elements if they already exist in the collectionConfig.xml. At present they need to be manually added there.
+  // it just copies them into storage.
+	static private void doSolrFacetsAndSorts(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		Element solr_element = to.createElement(StaticStrings.SOLR_ELEMENT);//<Solr>
+		solr_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+		solr_element.setAttribute(StaticStrings.MGPP_ATTRIBUTE, StaticStrings.TRUE_STR);
+
+
+		NodeList sort_children = ((Element) searchNode).getElementsByTagName(StaticStrings.SORT_LOW_STR);//<sort>
+		
+		int num_nodes = sort_children.getLength();
+
+		for (int i = 0; i < num_nodes; i++) {
+		  solr_element.appendChild(to.importNode((Element) sort_children.item(i), true));
+		}
+		Element sort_def = (Element)XMLTools.getChildByTagName(searchNode, StaticStrings.SORT_DEFAULT_ELEMENT);
+		if (sort_def != null) {
+		  solr_element.appendChild(to.importNode(sort_def, true));
+		}
+
+		NodeList facet_children = ((Element) searchNode).getElementsByTagName(StaticStrings.FACET_LOW_STR);//facet
+		num_nodes = facet_children.getLength();
+
+		for (int i = 0; i < num_nodes; i++)
+		{
+		  solr_element.appendChild(to.importNode((Element) facet_children.item(i), true));
+		}
+
+		// finished processing sort and facet elements
+		toElement.appendChild(solr_element);
+
+	
+	}
+
+	static private void doGlobalFormat(Document to, Element from)
+	{
+		// look for a top level format element
+		Element fe = (Element) XMLTools.getChildByTagName(from, StaticStrings.FORMAT_STR);
+		to.getDocumentElement().appendChild(doFormat(to, fe, StaticStrings.GLOBAL_STR));
+	}
+
+	static private void doDisplayFormat(Document to, Element from)
+	{
+		//display element in the xml file
+		Element de = (Element) XMLTools.getChildByTagName(from, StaticStrings.DISPLAY_STR);
+		if (de == null)
+		{
+			return;
+		}
+		//format element in the display element
+		Element fe = (Element) XMLTools.getChildByTagName(de, StaticStrings.FORMAT_STR);
+
+		to.getDocumentElement().appendChild(doFormat(to, fe, StaticStrings.DISPLAY_STR));
+	}
+
+	//construct 'DefaultIndex' element in the internal structure from collectionConfig.xml
+	static private void doDefaultIndex(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		Element default_index_element = to.createElement(StaticStrings.INDEX_DEFAULT_ELEMENT);
+		default_index_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+
+		Element e = (Element) XMLTools.getChildByTagName(searchNode, StaticStrings.INDEX_DEFAULT_ELEMENT_LOWERCASE);//defaultIndex
+		if (e == null)
+		{
+			return;
+		}
+		String index_str = e.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+
+		boolean old_index = false;
+		if (index_str.indexOf(StaticStrings.COLON_CHARACTER) != -1)
+		{
+			//The index is 'level:source tuplets' which is for mg. Take out 'level'
+			old_index = true;
+			default_index_element.setAttribute(StaticStrings.LEVEL_ATTRIBUTE, index_str.substring(0, index_str.indexOf(StaticStrings.COLON_CHARACTER)));
+			index_str = index_str.substring(index_str.indexOf(StaticStrings.COLON_CHARACTER) + 1);
+		}
+		else
+		{
+			default_index_element.setAttribute(StaticStrings.LEVEL_ATTRIBUTE, "");
+		}
+
+		//Each index may have a list of comma-separated strings.
+		//split them into 'content' elements in the internal structure
+		StringTokenizer content_tokenizer = new StringTokenizer(index_str, StaticStrings.COMMA_CHARACTER);
+		while (content_tokenizer.hasMoreTokens())
+		{
+			Element content_element = to.createElement(StaticStrings.CONTENT_ELEMENT);
+			String content_str = content_tokenizer.nextToken();
+			// Since the contents of indexes have to be certain keywords, or metadata elements, if the content isn't a keyword and doesn't yet have a namespace, append the extracted metadata namespace.
+			if (content_str.indexOf(StaticStrings.NS_SEP) == -1)
+			{
+				if (content_str.equals(StaticStrings.TEXT_STR) || (!old_index && content_str.equals(StaticStrings.ALLFIELDS_STR)))
+				{
+					// in this case, do nothing
+				}
+				else
+				{
+					content_str = StaticStrings.EXTRACTED_NAMESPACE + content_str;
+				}
+			}
+
+			content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, content_str);
+			default_index_element.appendChild(content_element);
+			content_element = null;
+		}
+		appendProperly(toElement, default_index_element);
+	}
+
+	// For mg, this method is still called, but make it 'assigned=false'
+	static private void doDefaultLevel(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		Element default_index_option = to.createElement(StaticStrings.INDEXOPTION_DEFAULT_ELEMENT);
+		default_index_option.setAttribute(StaticStrings.NAME_STR, StaticStrings.LEVEL_DEFAULT_STR);
+
+		Element e = (Element) XMLTools.getChildByTagName(searchNode, StaticStrings.LEVEL_DEFAULT_ELEMENT);
+		if (e != null)
+		{
+			default_index_option.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+			String level = e.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			default_index_option.setAttribute(StaticStrings.VALUE_ATTRIBUTE, level);
+		}
+		else
+		{
+			//In the case of mg, there's no level! build a default one using 'assigned=false value=document'
+			default_index_option.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+			default_index_option.setAttribute(StaticStrings.VALUE_ATTRIBUTE, StaticStrings.DOCUMENT_STR);
+		}
+		appendProperly(toElement, default_index_option);
+	}
+
+	// Transform plugins (pluginListNode) of collectionConfig.xml into the internal structure (i.e. Document to)
+	static private void doPlugins(Document to, Node pluginListNode)
+	{
+		Element toElement = to.getDocumentElement();
+		NodeList plugin_children = ((Element) pluginListNode).getElementsByTagName(StaticStrings.PLUGIN_STR);
+		int plugin_nodes = plugin_children.getLength();
+
+		if (plugin_nodes < 1)
+		{
+			return;
+		}
+
+		for (int i = 0; i < plugin_nodes; i++)
+		{
+			Element e = (Element) plugin_children.item(i);
+			String str = e.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			str = Utility.ensureNewPluginName(str);
+			Element plugin_element = to.createElement(StaticStrings.PLUGIN_ELEMENT);
+			plugin_element.setAttribute(StaticStrings.TYPE_ATTRIBUTE, str);
+
+			NodeList option_children = e.getElementsByTagName(StaticStrings.OPTION_STR);
+
+			for (int j = 0; j < option_children.getLength(); j++)
+			{
+				Element el = (Element) option_children.item(j);
+				String name_str = el.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+				if (name_str.startsWith(StaticStrings.MINUS_CHARACTER))
+				{
+					name_str = name_str.substring(1);
+				}
+				String value_str = el.getAttribute(StaticStrings.VALUE_ATTRIBUTE);
+				Element option_element = null;
+
+				if (name_str.equals("") && !value_str.equals(""))
+				{
+					continue;
+				}
+
+				option_element = to.createElement(StaticStrings.OPTION_ELEMENT);
+				option_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+				if (name_str.equals(StaticStrings.RECPLUG_STR) && value_str.equals(StaticStrings.USE_METADATA_FILES_ARGUMENT))
+				{
+					continue; // ignore this option
+				}
+
+				if (value_str != null)
+				{
+					// Remove any speech marks appended in strings containing whitespace
+					if (value_str.startsWith(StaticStrings.SPEECH_CHARACTER) && value_str.endsWith(StaticStrings.SPEECH_CHARACTER))
+					{
+						value_str = value_str.substring(1, value_str.length() - 1);
+					}
+					if (name_str.equals(StaticStrings.METADATA_STR))
+					{
+						// The metadata argument must be the fully qualified name of a metadata element, so if it doesn't yet have a namespace, append the extracted metadata namespace.
+						String[] values = value_str.split(StaticStrings.COMMA_CHARACTER);
+						value_str = "";
+						for (int k = 0; k <= values.length - 1; k++)
+						{
+							if (values[k].indexOf(StaticStrings.NS_SEP) == -1)
+							{
+								values[k] = StaticStrings.EXTRACTED_NAMESPACE + values[k];
+							}
+
+							if (k < values.length - 1)
+							{
+								value_str = value_str + values[k] + StaticStrings.COMMA_CHARACTER;
+
+							}
+							else
+							{
+								value_str = value_str + values[k];
+							}
+						}
+					}
+				}
+				if (!name_str.equals(""))
+				{
+					option_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+				}
+				if (!value_str.equals(""))
+				{
+					XMLTools.setNodeText(option_element, value_str);
+				}
+				plugin_element.appendChild(option_element);
+
+			}
+
+			appendProperly(toElement, plugin_element);
+		}
+
+	}
+
+	//Handle classifiers
+	static private void doClassifier(Document to, Node browseNode)
+	{
+		Element toElement = to.getDocumentElement();
+		NodeList classifier_children = ((Element) browseNode).getElementsByTagName(StaticStrings.CLASSIFIER_STR);
+		int num_nodes = classifier_children.getLength();
+
+		if (num_nodes < 1)
+		{
+			return;
+		}
+
+		for (int i = 0; i < num_nodes; i++)
+		{
+			Element e = (Element) classifier_children.item(i);
+			String str = e.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			Element classify_element = to.createElement(StaticStrings.CLASSIFY_ELEMENT);
+			classify_element.setAttribute(StaticStrings.TYPE_ATTRIBUTE, str);
+
+			String options_str = "";
+			NodeList option_children = e.getElementsByTagName(StaticStrings.OPTION_STR);
+			for (int j = 0; j < option_children.getLength(); j++)
+			{
+				Element el = (Element) option_children.item(j);
+				String name_str = el.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+				options_str = options_str + ((name_str.equals("")) ? "" : (" " + name_str));
+				if (name_str.startsWith(StaticStrings.MINUS_CHARACTER))
+				{
+					name_str = name_str.substring(1);
+				}
+				String value_str = el.getAttribute(StaticStrings.VALUE_ATTRIBUTE);
+				options_str = options_str + ((name_str.equals("")) ? "" : (" " + value_str));
+				Element option_element = null;
+
+				if (name_str.equals("") && !value_str.equals(""))
+				{
+					continue; //invalid condition
+				}
+
+				option_element = to.createElement(StaticStrings.OPTION_ELEMENT);
+				option_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+
+				if (!name_str.equals(""))
+				{
+					option_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+				}
+
+				if (!value_str.equals("") && name_str.equals(StaticStrings.METADATA_STR))
+				{
+					// The metadata argument must be the fully qualified name of a metadata element, so if it doesn't yet have a namespace, append the extracted metadata namespace.
+					String[] values = value_str.split(StaticStrings.COMMA_CHARACTER);
+					value_str = "";
+					for (int k = 0; k <= values.length - 1; k++)
+					{
+						if (values[k].indexOf(StaticStrings.NS_SEP) == -1)
+						{
+							values[k] = StaticStrings.EXTRACTED_NAMESPACE + values[k];
+						}
+						else
+						{
+							MetadataElement metadata_element = MetadataTools.getMetadataElementWithName(values[k]);
+							if (metadata_element != null)
+							{
+								values[k] = metadata_element.getDisplayName();
+							}
+						}
+						if (k < values.length - 1)
+						{
+							value_str = value_str + values[k] + StaticStrings.COMMA_CHARACTER;
+						}
+						else
+						{
+							value_str = value_str + values[k];
+						}
+					}
+				}
+
+				if (value_str != null && !value_str.equals(""))
+				{
+					XMLTools.setNodeText(option_element, value_str);
+				}
+				classify_element.appendChild(option_element);
+			}
+			//format element for this classifier
+			Element format = (Element) XMLTools.getChildByTagName(e, StaticStrings.FORMAT_STR);
+			if (format != null)
+			{
+				classify_element.appendChild(doFormat(to, format, null));
+			}
+			appendProperly(toElement, classify_element);
+		}
+
+		// default format statement for all classifiers
+		Element default_classifier_format = (Element) XMLTools.getChildByTagName(browseNode, StaticStrings.FORMAT_STR);
+
+		to.getDocumentElement().appendChild(doFormat(to, default_classifier_format, StaticStrings.BROWSE_STR));
+	}
+
+	static private Element doFormat(Document to, Element format, String name_str)
+	{
+		Element format_element = to.createElement(StaticStrings.FORMAT_STR);
+		if (name_str != null)
+		{
+			format_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+		}
+
+		// Don't write out an empty format statement of <format/> (i.e. format has no child nodes)
+		// as this will end up embedded in another format statement as <format><format/><format />
+		// This doubling up of format stmts will then prevent GLI from opening the collection again.
+		if (format != null && format.hasChildNodes())
+		{ // not an empty format statement
+			String gsf_text = XMLTools.xmlNodeToString(format);
+
+			//We don't want the <format> elements in the string
+			int startFormatIndex = gsf_text.indexOf(StaticStrings.FORMAT_START_TAG) + StaticStrings.FORMAT_START_TAG.length();
+			int endFormatIndex = gsf_text.lastIndexOf(StaticStrings.FORMAT_END_TAG);
+			gsf_text = gsf_text.substring(startFormatIndex, endFormatIndex);
+
+			XMLTools.setNodeText(format_element, gsf_text);
+		}
+		return format_element;
+	}
+
+	// Handling 'subcollection' elements in 'search' element of 'collectionConfig.xml'
+	static private void doSubcollection(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		NodeList sub_children = ((Element) searchNode).getElementsByTagName(StaticStrings.SUBCOLLECTION_STR);
+		int sub_nodes = sub_children.getLength();
+
+		// There is no subcollection
+		if (sub_nodes < 1)
+		{
+			return;
+		}
+
+		for (int i = 0; i < sub_nodes; i++)
+		{
+			Element sub_child = (Element) sub_children.item(i);
+			String name_str = sub_child.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			String filter_str = sub_child.getAttribute(StaticStrings.FILTER_ATTRIBUTE);
+
+			// filter_str is in the form '<! (if set)><metadata>/<metadata value>/<flag (if any)>'
+
+			int pos = filter_str.indexOf(StaticStrings.SEPARATOR_CHARACTER);
+			String meta_str = "";
+			String meta_value_str = "";
+			String clude_str = "";
+			String flag_str = "";
+			if (pos == -1)
+			{
+
+				meta_str = meta_value_str = filter_str;
+				clude_str = StaticStrings.INCLUDE_STR;
+			}
+			else
+			{
+				clude_str = StaticStrings.INCLUDE_STR;
+				if (filter_str.startsWith(StaticStrings.EXCLAMATION_CHARACTER))
+				{
+					clude_str = StaticStrings.EXCLUDE_STR;
+					// Peel off "!"
+					filter_str = filter_str.substring(StaticStrings.EXCLAMATION_CHARACTER.length());
+				}
+
+				String[] strs = filter_str.split(StaticStrings.SEPARATOR_CHARACTER);
+				if (strs[0] != null && strs[0] != "")
+				{
+					meta_str = strs[0];
+				}
+				if (!meta_str.equals(StaticStrings.FILENAME_STR) && meta_str.indexOf(StaticStrings.NS_SEP) == -1)
+				{
+					meta_str = StaticStrings.EXTRACTED_NAMESPACE + meta_str;
+				}
+
+				if (strs[1] != null && strs[1] != "")
+				{
+					meta_value_str = strs[1];
+				}
+				if (strs.length > 2)
+				{
+					//This means there has been set a flag
+					if (strs[2] != null && strs[2] != "")
+					{
+						flag_str = strs[2];
+					}
+				}
+			}
+			Element subcollection_element = to.createElement(StaticStrings.SUBCOLLECTION_ELEMENT);
+			subcollection_element.setAttribute(StaticStrings.NAME_STR, name_str);
+			subcollection_element.setAttribute(StaticStrings.CONTENT_ATTRIBUTE, meta_str);
+			subcollection_element.setAttribute(StaticStrings.TYPE_ATTRIBUTE, clude_str);
+			if (flag_str != "")
+			{
+				subcollection_element.setAttribute(StaticStrings.OPTIONS_ATTRIBUTE, flag_str);
+			}
+			XMLTools.setNodeText(subcollection_element, meta_value_str);
+
+			toElement.appendChild(subcollection_element);
+		}
+	}
+
+	//Handle levels (document, section). In the internal structure, the element is called 'IndexOption'
+	static private void doLevel(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		NodeList level_children = ((Element) searchNode).getElementsByTagName(StaticStrings.LEVEL_ATTRIBUTE);
+		int level_nodes = level_children.getLength();
+
+		// it's mg, there's no level. So we construct a default 'indexOption' in the internal structure
+		if (level_nodes < 1)
+		{
+			Element index_option = to.createElement(StaticStrings.INDEXOPTIONS_ELEMENT);
+			index_option.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+			index_option.setAttribute(StaticStrings.NAME_STR, StaticStrings.LEVELS_STR);
+
+			Element option_element = to.createElement(StaticStrings.OPTION_ELEMENT);
+			option_element.setAttribute(StaticStrings.NAME_STR, StaticStrings.DOCUMENT_STR);
+			index_option.appendChild(option_element);
+
+			appendProperly(toElement, index_option);
+
+			return;
+		}
+
+		Element index_option = to.createElement(StaticStrings.INDEXOPTIONS_ELEMENT);
+		index_option.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+		index_option.setAttribute(StaticStrings.NAME_STR, StaticStrings.LEVELS_STR);
+
+		for (int i = 0; i < level_nodes; i++)
+		{
+			Element level_element = (Element) level_children.item(i);
+			String level_str = level_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			Element option_element = to.createElement(StaticStrings.OPTION_ELEMENT);
+			option_element.setAttribute(StaticStrings.NAME_STR, level_str);
+			index_option.appendChild(option_element);
+
+			// Contructing 'collectionmetadata' elements from the 'displayItem' of this 'level' element
+			ArrayList displayItem_list = XMLTools.getNamedElementList(level_element, StaticStrings.DISPLAYITEM_STR, StaticStrings.NAME_ATTRIBUTE, StaticStrings.NAME_STR);
+			if (displayItem_list == null)
+			{
+				return;
+			}
+			for (int j = 0; j < displayItem_list.size(); j++)
+			{
+				Element item = (Element) displayItem_list.get(j);
+				String text = XMLTools.getNodeText(item);
+
+				//If there is nothing to display, don't bother creating the element
+				// Either lang or key should be set. If lang set, then should have textnode.
+				if (item.hasAttribute(StaticStrings.LANG_STR) && text.equals("")) {
+				    continue;
+				}
+				
+				String lang = item.getAttribute(StaticStrings.LANG_ATTRIBUTE);
+				String key = item.getAttribute(StaticStrings.KEY_ATTRIBUTE);
+			
+				Element collectionmetadata = to.createElement(StaticStrings.COLLECTIONMETADATA_ELEMENT);
+				collectionmetadata.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+				collectionmetadata.setAttribute(StaticStrings.NAME_ATTRIBUTE, level_str);
+
+				if(!lang.equals("")) {
+				    collectionmetadata.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, lang);
+				    XMLTools.setNodeText(collectionmetadata, text);
+				}
+				
+				if(!key.equals("")) {
+				    collectionmetadata.setAttribute(StaticStrings.KEY_ATTRIBUTE, key);
+				    String dictionary = item.getAttribute(StaticStrings.DICTIONARY_ATTRIBUTE);
+				    if(!dictionary.equals("")) {
+					collectionmetadata.setAttribute(StaticStrings.DICTIONARY_ATTRIBUTE, dictionary);
+				    }
+				}
+
+				appendProperly(toElement, collectionmetadata);
+			}
+		}
+		appendProperly(toElement, index_option);
+	}
+
+	//Handle 'indexSubcollection' element of collectionConfig.xml,  which is called 'SubcollectionIndexes' in the internal structure. These contain the subcollection indexes (i.e. the filter or filter combinations), and displayed words for the filter names.
+	static private void doIndexSubcollection(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		NodeList index_sub_children = ((Element) searchNode).getElementsByTagName(StaticStrings.SUBCOLLECTION_INDEX_ELEMENT);
+		int num_nodes = index_sub_children.getLength();
+
+		// there is no subcollection index
+		if (num_nodes < 1)
+		{
+			return;
+		}
+
+		Element subcollection_indexes = to.createElement(StaticStrings.SUBCOLLECTION_INDEXES_ELEMENT);
+
+		for (int i = 0; i < num_nodes; i++)
+		{
+			Element index_element = to.createElement(StaticStrings.INDEX_ELEMENT);
+			Element index_sub_child = (Element) index_sub_children.item(i);
+			String name_str = index_sub_child.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+
+			// name_str is in the form of comma separated strings, each of which is a subcollection filter name
+			String[] filters = name_str.split(StaticStrings.COMMA_CHARACTER);
+			for (int j = 0; j < filters.length; j++)
+			{
+
+				Element content_element = to.createElement(StaticStrings.CONTENT_ELEMENT);
+				content_element.setAttribute(StaticStrings.NAME_STR, filters[j]);
+				index_element.appendChild(content_element);
+			}
+			subcollection_indexes.appendChild(index_element);
+
+			// Contructing 'collectionmetadata' elements from the 'displayItem' of this 'indexSubcollection' element
+			ArrayList displayItem_list = XMLTools.getNamedElementList(index_sub_child, StaticStrings.DISPLAYITEM_STR, StaticStrings.NAME_ATTRIBUTE, StaticStrings.NAME_STR);
+			if (displayItem_list == null)
+			{
+				// there is no display item for this element
+				continue;
+			}
+			for (int j = 0; j < displayItem_list.size(); j++)
+			{
+				Element item = (Element) displayItem_list.get(j);
+				String text = XMLTools.getNodeText(item);
+				
+
+				//If there is nothing to display, don't bother creating the element
+				// Either lang or key should be set. If key set, no textnode.
+				// if lang set, should have textnode. Check if it's meaningful
+				if (item.hasAttribute(StaticStrings.LANG_STR) && text.equals(""))
+				{
+					continue;
+				}
+
+				String lang = item.getAttribute(StaticStrings.LANG_ATTRIBUTE);
+				String key = item.getAttribute(StaticStrings.KEY_ATTRIBUTE);
+
+				Element collectionmetadata = to.createElement(StaticStrings.COLLECTIONMETADATA_ELEMENT);
+				collectionmetadata.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+				collectionmetadata.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+				if(!lang.equals("")) {
+				    collectionmetadata.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, lang);
+				    XMLTools.setNodeText(collectionmetadata, text);
+				}
+				if(!key.equals("")) {
+				    collectionmetadata.setAttribute(StaticStrings.KEY_ATTRIBUTE, key);
+				    String dictionary = item.getAttribute(StaticStrings.DICTIONARY_ATTRIBUTE);
+				    if(!dictionary.equals("")) {
+					collectionmetadata.setAttribute(StaticStrings.DICTIONARY_ATTRIBUTE, dictionary);
+				    }
+				}
+				
+				appendProperly(toElement, collectionmetadata);
+			}
+		}
+		appendProperly(toElement, subcollection_indexes);
+	}
+
+	//Handle 'indexLanguage' element of collectionConfig.xml,  which is called 'Languages' in the internal structure. These contain the language indexes, and displayed words for those language index names.
+	static private void doIndexLanguage(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		NodeList index_sub_children = ((Element) searchNode).getElementsByTagName(StaticStrings.LANGUAGE_INDEX_ELEMENT);
+		int num_nodes = index_sub_children.getLength();
+
+		// there is no subcollection index
+		if (num_nodes < 1)
+		{
+			return;
+		}
+
+		Element language_indexes = to.createElement(StaticStrings.LANGUAGES_ELEMENT);
+
+		for (int i = 0; i < num_nodes; i++)
+		{
+			Element language_element = to.createElement(StaticStrings.LANGUAGE_ELEMENT);
+			Element index_sub_child = (Element) index_sub_children.item(i);
+			String name_str = index_sub_child.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			language_element.setAttribute(StaticStrings.NAME_STR, name_str);
+			language_indexes.appendChild(language_element);
+
+			// Contructing 'collectionmetadata' elements from the 'displayItem' of this 'indexLanguage' element
+			ArrayList displayItem_list = XMLTools.getNamedElementList(index_sub_child, StaticStrings.DISPLAYITEM_STR, StaticStrings.NAME_ATTRIBUTE, StaticStrings.NAME_STR);
+			if (displayItem_list == null)
+			{
+				// there is no display item for this element
+				continue;
+			}
+			for (int j = 0; j < displayItem_list.size(); j++)
+			{
+				Element item = (Element) displayItem_list.get(j);
+				String text = XMLTools.getNodeText(item);
+				//If there is nothing to display, don't bother creating the element
+				// Either lang or key should be set. If key set, no textnode.
+				// if lang set, should have textnode. Check if it's meaningful
+				if (item.hasAttribute(StaticStrings.LANG_STR) && text.equals(""))
+				{
+					continue;
+				}
+
+				// get the value in 'lang=langcode' or 'key=keyname'
+				// We can have either a key attribute (with optional dictionary attribute, else dictionary is implicit)
+				// OR we can have a lang attr, with the nodetext containing the actual display strin.g
+				String lang = item.getAttribute(StaticStrings.LANG_ATTRIBUTE);
+				String key = item.getAttribute(StaticStrings.KEY_ATTRIBUTE);
+
+
+				Element collectionmetadata = to.createElement(StaticStrings.COLLECTIONMETADATA_ELEMENT);
+				collectionmetadata.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+				collectionmetadata.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+
+				if(!lang.equals("")) {
+				    collectionmetadata.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, lang);
+				    XMLTools.setNodeText(collectionmetadata, text);
+				}
+				if(!key.equals("")) {
+				    collectionmetadata.setAttribute(StaticStrings.KEY_ATTRIBUTE, key);
+				    String dictionary = item.getAttribute(StaticStrings.DICTIONARY_ATTRIBUTE);
+				    if(!dictionary.equals("")) {
+					collectionmetadata.setAttribute(StaticStrings.DICTIONARY_ATTRIBUTE, dictionary);
+				    }
+				}
+
+				appendProperly(toElement, collectionmetadata);
+			}
+		}
+		toElement.appendChild(language_indexes);
+	}
+
+	// Handling search types
+	static private void doSearchType(Document to, Node searchNode)
+	{
+		NodeList type_children = ((Element) searchNode).getElementsByTagName(StaticStrings.SEARCHTYPE_ELEMENT);
+		int num_types = type_children.getLength();
+		String searchtype_str = "";
+		if (num_types < 1)
+		{
+			// not defined yet, add in default
+			searchtype_str = "plain,simpleform,advancedform";
+		}
+		else
+		{
+			for (int i = 0; i < num_types; i++)
+			{
+				Node e = type_children.item(i);
+				String t = ((Element) e).getAttribute(StaticStrings.NAME_ATTRIBUTE);
+				if (i > 0)
+				{
+					searchtype_str += ",";
+				}
+				searchtype_str += t;
+			}
+		}
+		searchtype_str = searchtype_str.trim();
+
+		// pretend its a format statement
+		Element search_type_element = to.createElement(StaticStrings.FORMAT_STR);
+		search_type_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.SEARCHTYPE_ELEMENT);
+		XMLTools.setNodeText(search_type_element, searchtype_str);
+		appendProperly(to.getDocumentElement(), search_type_element);
+
+	}
+
+	// Handling search format statement
+	static private void doSearchFormat(Document to, Node searchNode)
+	{
+		// THere is currently just one format element for search. HOwever, need to check for old config files which used to have <format name="searchTypes">
+		NodeList format_children = ((Element) searchNode).getElementsByTagName(StaticStrings.FORMAT_STR);
+		int format_nodes = format_children.getLength();
+		if (format_nodes < 1)
+		{
+			return;
+		}
+		Element format = null;
+		for (int i = 0; i < format_nodes; i++)
+		{
+			Node e = format_children.item(i);
+			if (e.hasAttributes() == false)
+			{
+				//The format element for format statement has no attribute
+				format = (Element) e;
+			}
+		}
+		//format statement for search
+		if (format != null)
+		{
+			(to.getDocumentElement()).appendChild(doFormat(to, format, StaticStrings.SEARCH_STR));
+		}
+	}
+
+	// Handling defaultIndexLanguage and languageMetadata in collectionConfig.xml ('elementNameFrom'); in the internal structure, they are called 'DefaultLanguage' and 'LanguageMetadata' ('elementNameTo') respectively.
+	// Converting from collectionConfig.xml to the internal xml structure.
+	static private void doLanguageMetadata(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		String elementNameFrom = StaticStrings.LANGUAGE_METADATA_ELEMENT_STR;
+		String elementNameTo = StaticStrings.LANGUAGE_METADATA_ELEMENT;
+		Node from_element = XMLTools.getChildByTagName(searchNode, elementNameFrom);
+		if (from_element == null)
+		{
+			return; // such an element not found
+		}
+
+		Element to_element = to.createElement(elementNameTo);
+
+		String name_str = ((Element) from_element).getAttribute(StaticStrings.NAME_ATTRIBUTE);
+		if (name_str.indexOf(StaticStrings.NS_SEP) == -1)
+		{
+			name_str = StaticStrings.EXTRACTED_NAMESPACE + name_str;
+		}
+		to_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+		to_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+
+		toElement.appendChild(to_element);
+	}
+
+	static private void doReplaceListRef(Document to, Element from)
+	{
+		Element toElement = to.getDocumentElement();
+
+		NodeList replace_elements = from.getElementsByTagName(StaticStrings.REPLACELISTREF_STR);
+		XMLTools.duplicateElementList(to, toElement, replace_elements, true);
+	}
+
+	// 	int num_elems = replace_elements.getLength();
+	// 	if (num_elems < 1)
+	// 	{
+	// 		return;
+	// 	}
+	// 	for (int i = 0; i < num_elems; i++)
+	// 	{
+	// 		Element to_element = XMLTools.duplicateElement(to, (Element) replace_elements.item(i), true);
+	// 		toElement.appendChild(to_element);
+	// 	}
+	// }
+
+	static private void convertReplaceListRef(Document from, Document to)
+	{
+		Element toElement = to.getDocumentElement();
+
+		NodeList replace_elements = from.getDocumentElement().getElementsByTagName(StaticStrings.REPLACELISTREF_STR);
+		XMLTools.duplicateElementList(to, toElement, replace_elements, true);
+	}
+
+	// int num_elems = replace_elements.getLength();
+	// if (num_elems < 1)
+	// {
+	// 	return;
+	// }
+	// for (int i = 0; i < num_elems; i++)
+	// {
+	// 	Element to_element = XMLTools.duplicateElement(to, (Element) replace_elements.item(i), true);
+	// 	toElement.appendChild(to_element);
+	// }
+	//	}
+
+	/*
+	 * replacelist currently not editable in GLI, just copy it in and back out
+	 * again
+	 */
+	static private void doReplaceList(Document to, Element from)
+	{
+		Element toElement = to.getDocumentElement();
+
+		Node rl_element = XMLTools.getChildByTagName(from, StaticStrings.REPLACELIST_STR);
+		if (rl_element == null)
+		{
+			return; // such an element not found
+		}
+
+		Element to_element = XMLTools.duplicateElement(to, (Element) rl_element, true);
+		toElement.appendChild(to_element);
+	}
+
+	static private void convertReplaceList(Document from, Document to)
+	{
+		Element toElement = to.getDocumentElement();
+
+		Node rl_element = XMLTools.getChildByTagName(from.getDocumentElement(), StaticStrings.REPLACELIST_STR);
+		if (rl_element == null)
+		{
+			return; // such an element not found
+		}
+
+		Element to_element = XMLTools.duplicateElement(to, (Element) rl_element, true);
+		toElement.appendChild(to_element);
+	}
+
+	/**
+	 * serviceRackList is currently not editable in GLI - just copy it in from
+	 * config file and write it out again.
+	 */
+	static private void doServiceRackList(Document to, Element from)
+	{
+		Element toElement = to.getDocumentElement();
+
+		Node srl_element = XMLTools.getChildByTagName(from, StaticStrings.SERVICE_RACK_LIST_ELEMENT);
+		if (srl_element == null)
+		{
+			return; // such an element not found
+		}
+
+		Element to_element = XMLTools.duplicateElement(to, (Element) srl_element, true);
+		toElement.appendChild(to_element);
+	}
+
+	static private void convertServiceRackList(Document from, Document to)
+	{
+		Element toElement = to.getDocumentElement();
+
+		Node srl_element = XMLTools.getChildByTagName(from.getDocumentElement(), StaticStrings.SERVICE_RACK_LIST_ELEMENT);
+		if (srl_element == null)
+		{
+			return; // such an element not found
+		}
+
+		Element to_element = XMLTools.duplicateElement(to, (Element) srl_element, true);
+		toElement.appendChild(to_element);
+	}
+
+	static private void doDefaultIndexLanguage(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		String elementNameFrom = StaticStrings.LANGUAGE_DEFAULT_INDEX_ELEMENT;
+		String elementNameTo = StaticStrings.LANGUAGE_DEFAULT_ELEMENT;
+		Node from_element = XMLTools.getChildByTagName(searchNode, elementNameFrom);
+		if (from_element == null)
+		{
+			return; // such an element not found
+		}
+
+		Element to_element = to.createElement(elementNameTo);
+
+		String name_str = ((Element) from_element).getAttribute(StaticStrings.NAME_ATTRIBUTE);
+		to_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+		to_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+
+		toElement.appendChild(to_element);
+	}
+
+	//Handle 'indexOption' (i.e. casefold, stem etc). In the internal structure, the element is called 'IndexOption'
+	static private void doIndexOption(Document to, Node searchNode)
+	{
+		Element toElement = to.getDocumentElement();
+		//Node index_option_node = XMLTools.getChildByTagName(searchNode, StaticStrings.INDEXOPTION_STR);
+		//if (index_option_node == null)
+		//{
+		//	return;
+		//}
+		//NodeList option_children = ((Element) index_option_node).getElementsByTagName(StaticStrings.OPTION_STR);
+		NodeList option_children = ((Element) searchNode).getElementsByTagName(StaticStrings.INDEXOPTION_STR);
+		int num_options = option_children.getLength();
+
+		// for lucene, there is no 'indexOption'. We build a default 'indexOption' and 'assigned=false' in case the user switches to mg or mgpp
+		if (num_options < 1)
+		{
+			Element index_option = to.createElement(StaticStrings.INDEXOPTIONS_ELEMENT);
+			index_option.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+			index_option.setAttribute(StaticStrings.NAME_STR, StaticStrings.INDEXOPTIONS_STR);
+			String[] option_str = { StaticStrings.CASEFOLD_OPTION_STR, StaticStrings.STEM_OPTION_STR };
+			for (int i = 0; i < option_str.length; i++)
+			{
+				Element option_element = to.createElement(StaticStrings.OPTION_ELEMENT);
+				option_element.setAttribute(StaticStrings.NAME_STR, option_str[i]);
+				index_option.appendChild(option_element);
+			}
+			appendProperly(toElement, index_option);
+			return;
+		}
+
+		Element index_option = to.createElement(StaticStrings.INDEXOPTIONS_ELEMENT);
+		index_option.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+		index_option.setAttribute(StaticStrings.NAME_STR, StaticStrings.INDEXOPTIONS_STR);
+
+		for (int i = 0; i < num_options; i++)
+		{
+			String option_str = ((Element) option_children.item(i)).getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			Element option_element = to.createElement(StaticStrings.OPTION_ELEMENT);
+			option_element.setAttribute(StaticStrings.NAME_STR, option_str);
+			index_option.appendChild(option_element);
+		}
+		appendProperly(toElement, index_option);
+	}
+
+	static private Element doBuildType(Document to, String att_value)
+	{
+
+		//construct 'BuildType' element
+		Element element = to.createElement(StaticStrings.BUILDTYPE_ELEMENT);
+		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.BUILDTYPE_STR);
+		element.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, StaticStrings.ENGLISH_LANGUAGE_STR);
+		element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
+
+		XMLTools.setNodeText(element, att_value);
+
+		return element;
+	}
+
+	static private Element doDatabaseType(Document to, String att_value)
+	{
+
+		//construct 'DatabaseType' element
+		Element element = to.createElement(StaticStrings.DATABASETYPE_ELEMENT);
+		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.DATABASETYPE_STR);
+		element.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, StaticStrings.ENGLISH_LANGUAGE_STR);
+		element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
+
+		XMLTools.setNodeText(element, att_value);
+
+		return element;
+	}
+
+	// Convert 'description', 'smallicon' etc.
+	static private void convertDisplayItemList(Document from, Document to)
+	{
+		Element displayItemList = to.createElement(StaticStrings.DISPLAYITEMLIST_STR);
+		Element destination = to.getDocumentElement();
+
+		// certain special collectionmeta elements should have different names
+		// as displayItems in the collectionConfig.xml than they do in memory
+		Map attributeMap = new HashMap(4);
+		attributeMap.put(StaticStrings.COLLECTIONMETADATA_COLLECTIONEXTRA_STR, StaticStrings.DESCRIPTION_STR);
+		attributeMap.put(StaticStrings.COLLECTIONMETADATA_COLLECTIONNAME_STR, StaticStrings.NAME_STR);
+		attributeMap.put(StaticStrings.COLLECTIONMETADATA_ICONCOLLECTIONSMALL_STR, StaticStrings.SMALLICON_STR);
+		attributeMap.put(StaticStrings.COLLECTIONMETADATA_ICONCOLLECTION_STR, StaticStrings.ICON_STR);
+
+		NodeList e_list = from.getDocumentElement().getElementsByTagName(StaticStrings.COLLECTIONMETADATA_ELEMENT);
+		// if such elements don't exist, don't bother
+		if (e_list != null)
+		{
+
+			for (int j = 0; j < e_list.getLength(); j++)
+			{
+				Element e = (Element) e_list.item(j);
+				if (e.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+				{
+					continue;
+				}
+				String text = XMLTools.getNodeText(e);
+				String lang = e.getAttribute(StaticStrings.LANGUAGE_ATTRIBUTE);
+				String key = e.getAttribute(StaticStrings.KEY_ATTRIBUTE);
+				String dictionary = e.getAttribute(StaticStrings.DICTIONARY_ATTRIBUTE);
+
+				String name_value = e.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+				String name_mapping = (String) attributeMap.get(name_value);
+				if (name_mapping != null)
+				{
+					name_value = name_mapping;
+				}
+
+				Element displayItem = constructElement(StaticStrings.DISPLAYITEM_STR, name_value, StaticStrings.LANG_STR, lang, text, StaticStrings.KEY_ATTRIBUTE, key, StaticStrings.DICTIONARY_ATTRIBUTE, dictionary, to);
+				//Element displayItem = constructDisplayItem(e, to, name_value);
+				displayItemList.appendChild(displayItem);
+			}
+
+		}
+		destination.appendChild(displayItemList);
+	}
+
+	// This method creates a DisplayItem element of the type of 'to' by using the ingredients from the element 'e'
+    static private Element constructDisplayItem(Element e, Document to, String name)
+	{
+		String lang_string = e.getAttribute(StaticStrings.LANGUAGE_ATTRIBUTE);
+		String key_string = e.getAttribute(StaticStrings.KEY_ATTRIBUTE);
+		String dictionary_string = e.getAttribute(StaticStrings.DICTIONARY_ATTRIBUTE);
+		String text = XMLTools.getNodeText(e);
+
+		Element displayItem = to.createElement(StaticStrings.DISPLAYITEM_STR);
+		displayItem.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+
+		if(!lang_string.equals("")) {
+		    displayItem.setAttribute(StaticStrings.LANG_STR, lang_string);
+		    XMLTools.setNodeText(displayItem, text);
+		}
+		if(!key_string.equals("")) {
+		    displayItem.setAttribute(StaticStrings.KEY_ATTRIBUTE, key_string);
+		    if(!dictionary_string.equals("")) {
+			displayItem.setAttribute(StaticStrings.DICTIONARY_ATTRIBUTE, dictionary_string);
+		    }
+		}
+		return displayItem;
+	}
+
+    static private Element constructDisplayItem(Element e, Document to)
+	{	
+	    return constructDisplayItem(e, to, StaticStrings.NAME_ATTRIBUTE);
+	}
+
+  static private void convertSecurity(Document from, Document to)
+	{
+	  Node security = XMLTools.getChildByTagNameIndexed(from.getDocumentElement(), StaticStrings.SECURITY_STR, 0);
+	  if (security != null)
+	    {
+	      Element to_element = XMLTools.duplicateElement(to, (Element) security, true);
+	      to.getDocumentElement().appendChild(to_element);
+	    
+	    }
+	}
+	static private void convertMetadataList(Document from, Document to)
+	{
+		Element metadataList = to.createElement(StaticStrings.METADATALIST_STR);
+		Element destination = to.getDocumentElement();
+
+		String[] ele_names = { StaticStrings.COLLECTIONMETADATA_CREATOR_ELEMENT, StaticStrings.COLLECTIONMETADATA_MAINTAINER_ELEMENT, StaticStrings.COLLECTIONMETADATA_PUBLIC_ELEMENT };
+		String[] att_names = { StaticStrings.COLLECTIONMETADATA_CREATOR_STR, StaticStrings.COLLECTIONMETADATA_MAINTAINER_STR, StaticStrings.COLLECTIONMETADATA_PUBLIC_STR };
+		for (int i = 0; i < ele_names.length; i++)
+		{
+			Element e = XMLTools.getNamedElement(from.getDocumentElement(), ele_names[i], StaticStrings.NAME_ATTRIBUTE, att_names[i]);
+			if (e == null)
+			{
+				continue;
+			}
+			String text = XMLTools.getNodeText(e);
+			Element metadata = to.createElement(StaticStrings.METADATA_STR);
+			metadata.setAttribute(StaticStrings.NAME_ATTRIBUTE, att_names[i]);
+			metadata.setAttribute(StaticStrings.LANG_STR, StaticStrings.ENGLISH_LANGUAGE_STR);
+			XMLTools.setNodeText(metadata, text);
+			metadataList.appendChild(metadata);
+		}
+
+		destination.appendChild(metadataList);
+	}
+
+	// This method creates an element with the name 'element_name' of the type of 'to' by using the other three strings
+    static private Element constructElement(String element_name, String name_value, String lang_att, String lang_value, String text, String key_att, String key, String dict_att, String dictionary, Document to)
+	{
+		Element e = to.createElement(element_name);
+		e.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+		e.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_value);
+
+		if(!lang_value.equals("")) {
+		    e.setAttribute(lang_att, lang_value);
+		    XMLTools.setNodeText(e, text);
+		}		
+		if(!key.equals("")) {
+		    e.setAttribute(key_att, key);
+		    if(!dictionary.equals("")) {
+			e.setAttribute(dict_att, dictionary);
+		    }
+		}
+
+		return e;
+	}
+
+	// Convert classify in the internal(i.e. Document from) to collectionconfig.xml (i.e. Document to)
+	static private void convertClassifier(Document from, Document to)
+	{
+		Element browse_element = to.createElement(StaticStrings.BROWSE_STR);
+		NodeList children = from.getDocumentElement().getElementsByTagName(StaticStrings.CLASSIFY_ELEMENT);
+
+		int num_children = (children == null) ? 0 : children.getLength();
+
+		if (num_children == 0)
+		{
+			return;
+		}
+
+		for (int i = 0; i < num_children; i++)
+		{
+
+			Element child = (Element) children.item(i);
+			if (child.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+			{
+				continue;
+			}
+			String str = child.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+			Element classifier_element = to.createElement(StaticStrings.CLASSIFIER_STR);
+			classifier_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, str);
+
+			NodeList option_children = child.getElementsByTagName(StaticStrings.OPTION_ELEMENT);
+			for (int j = 0; j < option_children.getLength(); j++)
+			{
+				Element el = (Element) option_children.item(j);
+				if (el.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+				{
+					continue;
+				}
+				String name_str = el.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+				String value_str = XMLTools.getNodeText(el);
+
+				if (name_str == null && value_str == null)
+				{
+					continue;
+				}
+				Element option_element = to.createElement(StaticStrings.OPTION_STR);
+				if (name_str != null && name_str.equals(StaticStrings.METADATA_STR))
+				{
+
+					// The metadata argument is the fully qualified name of a metadata element, so if it contains a namespace, remove the extracted metadata namespace as the build process doesn't know about it.
+					String[] values = value_str.split(StaticStrings.COMMA_CHARACTER);
+					value_str = "";
+					for (int k = 0; k <= values.length - 1; k++)
+					{
+						if (values[k].startsWith(StaticStrings.EXTRACTED_NAMESPACE) && values[k].indexOf(StaticStrings.NS_SEP, StaticStrings.EXTRACTED_NAMESPACE.length()) == -1)
+						{
+							values[k] = values[k].substring(StaticStrings.EXTRACTED_NAMESPACE.length());
+						}
+						else
+						{
+							MetadataElement metadata_element = MetadataTools.getMetadataElementWithDisplayName(values[k]);
+							if (metadata_element != null)
+							{
+								values[k] = metadata_element.getFullName();
+							}
+						}
+						if (k < values.length - 1)
+						{
+							value_str = value_str + values[k] + StaticStrings.COMMA_CHARACTER;
+						}
+						else
+						{
+							value_str = value_str + values[k];
+						}
+					}
+				}
+
+				if (!name_str.equals(""))
+				{
+					if (!name_str.startsWith(StaticStrings.MINUS_CHARACTER))
+					{
+						name_str = StaticStrings.MINUS_CHARACTER + name_str;
+					}
+					option_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+				}
+
+				if (!value_str.equals(""))
+				{
+					option_element.setAttribute(StaticStrings.VALUE_ATTRIBUTE, value_str);
+				}
+
+				classifier_element.appendChild(option_element);
+			}
+
+			//format element for this classifier
+			Element e = (Element) XMLTools.getChildByTagName(child, StaticStrings.FORMAT_STR);
+
+			if (e != null)
+			{
+				classifier_element.appendChild(convertFormat(to, e));
+			}
+			browse_element.appendChild(classifier_element);
+		}
+
+		//convert default classifier format
+		Element e = XMLTools.getNamedElement(from.getDocumentElement(), StaticStrings.FORMAT_STR, StaticStrings.NAME_ATTRIBUTE, StaticStrings.BROWSE_STR);
+		browse_element.appendChild(convertFormat(to, e));
+
+		to.getDocumentElement().appendChild(browse_element);
+	}
+
+	static private Element convertFormat(Document to, Element e)
+	{
+		String format_str = XMLTools.getNodeText(e);
+		Element format = to.createElement(StaticStrings.FORMAT_STR);
+		//XMLTools.copyAllChildren (format, e);
+		XMLTools.setNodeText(format, format_str);
+		return format;
+	}
+
+	//convert format statement for search
+	static private void convertSearchFormat(Document from, Document to)
+	{
+		Element search = (Element) XMLTools.getChildByTagName(to.getDocumentElement(), StaticStrings.SEARCH_STR);
+		Element e = XMLTools.getNamedElement(from.getDocumentElement(), StaticStrings.FORMAT_STR, StaticStrings.NAME_ATTRIBUTE, StaticStrings.SEARCH_STR);
+
+		search.appendChild(convertFormat(to, e));
+
+	}
+
+	//convert format statement for display of the documents
+	static private void convertDisplayFormat(Document from, Document to)
+	{
+		Element e = XMLTools.getNamedElement(from.getDocumentElement(), StaticStrings.FORMAT_STR, StaticStrings.NAME_ATTRIBUTE, StaticStrings.DISPLAY_STR);
+		if (e == null)
+		{
+			return;
+		}
+		Element display = to.createElement(StaticStrings.DISPLAY_STR);
+		display.appendChild(convertFormat(to, e));
+		to.getDocumentElement().appendChild(display);
+	}
+
+	// convert global format statement
+	static private void convertGlobalFormat(Document from, Document to)
+	{
+		Element e = XMLTools.getNamedElement(from.getDocumentElement(), StaticStrings.FORMAT_STR, StaticStrings.NAME_ATTRIBUTE, StaticStrings.GLOBAL_STR);
+
+		to.getDocumentElement().appendChild(convertFormat(to, e));
+
+	}
+
+	// Convert plugins in the internal(i.e. Document from) to collectionconfig.xml (i.e. Document to)
+	static private void convertPlugins(Document from, Document to)
+	{
+		Element import_element = to.createElement(StaticStrings.IMPORT_STR);
+		Element plugin_list_element = to.createElement(StaticStrings.PLUGINLIST_STR);
+
+		NodeList children = from.getDocumentElement().getElementsByTagName(StaticStrings.PLUGIN_ELEMENT);
+		int num_children = (children == null) ? 0 : children.getLength();
+		if (num_children == 0)
+		{
+			return;
+		}
+
+		for (int i = 0; i < num_children; i++)
+		{
+
+			Element child = (Element) children.item(i);
+			if (child.getAttribute(StaticStrings.SEPARATOR_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
+			{
+				continue;
+			}
+			if (child.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+			{
+				continue;
+			}
+
+			String str = child.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+			Element plugin_element = to.createElement(StaticStrings.PLUGIN_STR);
+			plugin_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, str);
+
+			NodeList option_children = child.getElementsByTagName(StaticStrings.OPTION_ELEMENT);
+			for (int j = 0; j < option_children.getLength(); j++)
+			{
+				Element el = (Element) option_children.item(j);
+				if (!el.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
+				{
+					continue;
+				}
+				String name_str = el.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+				String value_str = XMLTools.getNodeText(el);
+
+				if (name_str == null && value_str == null)
+				{
+					continue;
+				}
+				Element option_element = to.createElement(StaticStrings.OPTION_STR);
+				if (name_str != null && name_str.equals(StaticStrings.METADATA_STR))
+				{
+
+					// The metadata argument is the fully qualified name of a metadata element, so if it contains a namespace, remove the extracted metadata namespace as the build process doesn't know about it, but ONLY if it is not embedded metadata (e.g. ex.dc.*)
+					String[] values = value_str.split(StaticStrings.COMMA_CHARACTER);
+					value_str = "";
+					for (int k = 0; k <= values.length - 1; k++)
+					{
+						if (values[k].startsWith(StaticStrings.EXTRACTED_NAMESPACE) && values[k].indexOf(StaticStrings.NS_SEP, StaticStrings.EXTRACTED_NAMESPACE.length()) == -1)
+						{
+							values[k] = values[k].substring(StaticStrings.EXTRACTED_NAMESPACE.length());
+						}
+
+						if (k < values.length - 1)
+						{
+							value_str = value_str + values[k] + StaticStrings.COMMA_CHARACTER;
+						}
+						else
+						{
+							value_str = value_str + values[k];
+						}
+					}
+				}
+
+				if (!name_str.equals(""))
+				{
+					if (!name_str.startsWith(StaticStrings.MINUS_CHARACTER))
+					{
+						name_str = StaticStrings.MINUS_CHARACTER + name_str;
+					}
+					option_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+				}
+
+				if (!value_str.equals(""))
+				{
+					option_element.setAttribute(StaticStrings.VALUE_ATTRIBUTE, value_str);
+				}
+
+				plugin_element.appendChild(option_element);
+			}//for loop ends
+
+			plugin_list_element.appendChild(plugin_element);
+		}//for loop ends
+
+		import_element.appendChild(plugin_list_element);
+
+		//do the plugout element (used by building flax collections)
+		Node plugout = XMLTools.getChildByTagNameIndexed(from.getDocumentElement(), PLUGOUT_ELEMENT, 0);
+		if (plugout != null)
+		{
+			Element to_element = XMLTools.duplicateElement(to, (Element) plugout, true);
+			import_element.appendChild(to_element);
+		}
+
+		to.getDocumentElement().appendChild(import_element);
+	}
+
+	//Handle 'searchType' of collectionConfig.xml. In the internal structure, its also called 'searchType', eg. plain, form
+	static private void convertSearchType(Document from, Document to)
+	{
+		Element e = XMLTools.getNamedElement(from.getDocumentElement(), StaticStrings.FORMAT_STR, StaticStrings.NAME_ATTRIBUTE, StaticStrings.SEARCHTYPE_ELEMENT);//searchType
+
+		if (e == null)
+		{
+			return;
+		}
+		String searchtype_str = XMLTools.getNodeText(e).trim();
+		//Get the 'search' element from 'to'
+		Element search = (Element) XMLTools.getChildByTagName(to.getDocumentElement(), StaticStrings.SEARCH_STR);
+
+		String[] types = searchtype_str.split(",");
+		for (int i = 0; i < types.length; i++)
+		{
+			Element search_type_element = to.createElement(StaticStrings.SEARCHTYPE_ELEMENT);
+			search_type_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, types[i]);
+			search.appendChild(search_type_element);
+		}
+	}
+
+	static private void convertBuildType(Document from, Document to)
+	{
+		Element e = XMLTools.getNamedElement(from.getDocumentElement(), StaticStrings.BUILDTYPE_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.BUILDTYPE_STR);
+		if (e == null)
+		{
+			return;
+		}
+		String indexer = XMLTools.getNodeText(e);
+		Element search = to.createElement(StaticStrings.SEARCH_STR);
+		search.setAttribute(StaticStrings.TYPE_ATTRIBUTE, indexer);
+		to.getDocumentElement().appendChild(search);
+	}
+
+	static private void convertDatabaseType(Document from, Document to)
+	{
+		Element e = XMLTools.getNamedElement(from.getDocumentElement(), StaticStrings.DATABASETYPE_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.DATABASETYPE_STR);
+		if (e == null)
+		{
+			return;
+		}
+		String db = XMLTools.getNodeText(e);
+		Element dbtype = to.createElement(StaticStrings.INFODB_STR);
+		dbtype.setAttribute(StaticStrings.TYPE_ATTRIBUTE, db);
+		to.getDocumentElement().appendChild(dbtype);
+	}
+
+	static private void convertDefaultIndex(Document from, Document to, Element search)
+	{
+		Element source = from.getDocumentElement();
+
+		Element default_index_element = (Element) XMLTools.getChildByTagName(source, StaticStrings.INDEX_DEFAULT_ELEMENT);
+		if (default_index_element == null)
+		{
+			return;
+		}
+
+		String indexer = search.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+		String level_str = default_index_element.getAttribute(StaticStrings.LEVEL_ATTRIBUTE);
+		// Debugging purposes
+		if (level_str.equals("") && indexer.equals(StaticStrings.MG_STR))
+		{
+			System.out.println("Bug: DefaultIndex should have its level attribute not empty.");
+		}
+
+		NodeList content_elements = default_index_element.getElementsByTagName(StaticStrings.CONTENT_ELEMENT);
+		int content_elements_length = content_elements.getLength();
+
+		// Don't output anything if no indexes are set
+		if (content_elements_length == 0)
+		{
+			return;//
+		}
+
+		String index_str = "";
+
+		if (indexer.equals(StaticStrings.MG_STR))
+		{
+			//combine level with indexes
+			index_str = level_str + StaticStrings.COLON_CHARACTER;
+		}
+		else
+		{ //for mgpp/lucene, just take index
+			//do nothing
+		}
+
+		for (int k = 0; k < content_elements_length; k++)
+		{
+			Element content_element = (Element) content_elements.item(k);
+			if (content_element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+			{
+				continue;
+			}
+			String name_str = content_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+
+			if (name_str.startsWith(StaticStrings.EXTRACTED_NAMESPACE) && name_str.indexOf(StaticStrings.NS_SEP, StaticStrings.EXTRACTED_NAMESPACE.length()) == -1)
+			{
+				name_str = name_str.substring(StaticStrings.EXTRACTED_NAMESPACE.length());
+			}
+
+			index_str = index_str + name_str;
+
+			// Make it comma separated string
+			if (k < content_elements_length - 1)
+			{
+				index_str = index_str + StaticStrings.COMMA_CHARACTER;
+			}
+			content_element = null;
+		}//for loop ends
+
+		Element default_index = to.createElement(StaticStrings.INDEX_DEFAULT_ELEMENT_LOWERCASE);
+		default_index.setAttribute(StaticStrings.NAME_ATTRIBUTE, index_str);
+		search.appendChild(default_index);
+
+	}
+
+	static private void convertSubcollection(Document from, Document to)
+	{
+		Element source = from.getDocumentElement();
+		//Get the 'search' element from 'to' which has already been created in 'convertBuildType'
+		Element search = (Element) XMLTools.getChildByTagName(to.getDocumentElement(), StaticStrings.SEARCH_STR);
+
+		// Get the Subcollection element from the internal structure
+		NodeList subcollection_elements = source.getElementsByTagName(StaticStrings.SUBCOLLECTION_ELEMENT);
+		if (subcollection_elements == null)
+		{
+			return;
+		}
+		int subcollection_elements_length = subcollection_elements.getLength();
+
+		if (subcollection_elements_length == 0)
+		{ // no
+			return;
+		}
+
+		for (int j = 0; j < subcollection_elements_length; j++)
+		{
+
+			Element e = (Element) subcollection_elements.item(j);
+			if (e.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+			{
+				continue;
+			}
+			String content = e.getAttribute(StaticStrings.CONTENT_ATTRIBUTE);
+			String name = e.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			String options = e.getAttribute(StaticStrings.OPTIONS_ATTRIBUTE);
+			String type = e.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+			String text = XMLTools.getNodeText(e);
+
+			String filter = "";
+			if (type.equals(StaticStrings.EXCLUDE_STR))
+			{
+				filter = StaticStrings.EXCLAMATION_CHARACTER;
+			}
+
+			if (content.startsWith(StaticStrings.EXTRACTED_NAMESPACE) && content.indexOf(StaticStrings.NS_SEP, StaticStrings.EXTRACTED_NAMESPACE.length()) == -1)
+			{
+				content = content.substring(StaticStrings.EXTRACTED_NAMESPACE.length());
+			}
+			filter = filter + content + StaticStrings.SEPARATOR_CHARACTER + text;
+			if (options != null && options != "")
+			{
+				filter = filter + StaticStrings.SEPARATOR_CHARACTER + options;
+			}
+			Element subcollection = to.createElement(StaticStrings.SUBCOLLECTION_STR);
+			subcollection.setAttribute(StaticStrings.FILTER_ATTRIBUTE, filter);
+			subcollection.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+
+			search.appendChild(subcollection);
+		}
+	}
+
+	static private void convertSubcollectionIndexes(Document from, Document to)
+	{
+		Element source = from.getDocumentElement();
+		//Get the 'search' element from 'to' which has already been created in 'convertBuildType'
+		Element search = (Element) XMLTools.getChildByTagName(to.getDocumentElement(), StaticStrings.SEARCH_STR);
+
+		// Get the SubcollectionIndexes element from the internal structure
+		Element subcollection_indexes = (Element) XMLTools.getChildByTagName(source, StaticStrings.SUBCOLLECTION_INDEXES_ELEMENT);
+		if (subcollection_indexes == null)
+		{
+			return;
+		}
+		NodeList index_elements = subcollection_indexes.getElementsByTagName(StaticStrings.INDEX_ELEMENT);
+		int index_elements_length = index_elements.getLength();
+
+		if (index_elements_length == 0)
+		{ // no indexes
+			return;
+		}
+
+		for (int j = 0; j < index_elements_length; j++)
+		{
+			Element index_element = (Element) index_elements.item(j);
+			if (index_element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+			{
+				continue;
+			}
+
+			Element index = to.createElement(StaticStrings.SUBCOLLECTION_INDEX_ELEMENT);
+
+			String index_value = "";
+
+			NodeList content_elements = index_element.getElementsByTagName(StaticStrings.CONTENT_ELEMENT);
+			int content_elements_length = content_elements.getLength();
+
+			for (int k = 0; k < content_elements_length; k++)
+			{
+				Element content_element = (Element) content_elements.item(k);
+				if (content_element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+				{
+					continue;
+				}
+				String name_str = content_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+				index_value += name_str;
+				// Make it comma separated string
+				if (k < content_elements_length - 1)
+				{
+					index_value += StaticStrings.COMMA_CHARACTER;
+				}
+				content_element = null;
+			}//for loop ends
+
+			index.setAttribute(StaticStrings.NAME_ATTRIBUTE, index_value);
+
+			// Now constructing 'displayItem' element for this 'indexSubcollection' element
+			// from the collectionmetadata element
+			ArrayList collectionmetadata_list = XMLTools.getNamedElementList(source, StaticStrings.COLLECTIONMETADATA_ELEMENT, StaticStrings.NAME_ATTRIBUTE, index_value);
+
+			if (collectionmetadata_list != null)
+			{
+
+				for (int k = 0; k < collectionmetadata_list.size(); k++)
+				{
+					Element collectionmetadata = (Element) collectionmetadata_list.get(k);
+					if (collectionmetadata.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+					{
+						continue;
+					}
+					Element displayItem = constructDisplayItem(collectionmetadata, to);
+					index.appendChild(displayItem);
+				}
+			}
+
+			search.appendChild(index);
+
+		} //for loop ends
+	}
+
+	static private void convertLanguages(Document from, Document to)
+	{
+		Element source = from.getDocumentElement();
+		//Get the 'search' element from 'to' which has already been created in 'convertBuildType'
+		Element search = (Element) XMLTools.getChildByTagName(to.getDocumentElement(), StaticStrings.SEARCH_STR);
+
+		// Get the Languages element from the internal structure
+		Element languages = (Element) XMLTools.getChildByTagName(source, StaticStrings.LANGUAGES_ELEMENT);
+		if (languages == null)
+		{
+			return;
+		}
+		NodeList language_elements = languages.getElementsByTagName(StaticStrings.LANGUAGE_ELEMENT);
+		int language_elements_length = language_elements.getLength();
+
+		if (language_elements_length == 0)
+		{
+			return;
+		}
+
+		for (int j = 0; j < language_elements_length; j++)
+		{
+			Element element = (Element) language_elements.item(j);
+			if (element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+			{
+				continue;
+			}
+
+			// Create indexLanguage element
+			Element index_language = to.createElement(StaticStrings.LANGUAGE_INDEX_ELEMENT);
+
+			String name_str = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			index_language.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+
+			// Now constructing 'displayItem' element for this 'indexLanguage' element
+			// from the collectionmetadata element
+			ArrayList collectionmetadata_list = XMLTools.getNamedElementList(source, StaticStrings.COLLECTIONMETADATA_ELEMENT, StaticStrings.NAME_ATTRIBUTE, name_str);
+
+			if (collectionmetadata_list != null)
+			{
+
+				for (int k = 0; k < collectionmetadata_list.size(); k++)
+				{
+					Element collectionmetadata = (Element) collectionmetadata_list.get(k);
+					if (collectionmetadata.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+					{
+						continue;
+					}
+					Element displayItem = constructDisplayItem(collectionmetadata, to);
+					index_language.appendChild(displayItem);
+				}
+			}
+
+			search.appendChild(index_language);
+
+		} //for loop ends
+
+		// Convert DefaultLanguage
+		// Get the DefaultLanguage element from the internal structure
+		Element default_language = (Element) XMLTools.getChildByTagName(source, StaticStrings.LANGUAGE_DEFAULT_ELEMENT);
+		if (default_language != null)
+		{
+			String lang_name = default_language.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			Element default_index_language = to.createElement(StaticStrings.LANGUAGE_DEFAULT_INDEX_ELEMENT);
+			default_index_language.setAttribute(StaticStrings.NAME_ATTRIBUTE, lang_name);
+			search.appendChild(default_index_language);
+		}
+		// Convert LanguageMetadata
+		// Get the LanguageMetadata element from the internal structure
+		Element language_metadata = (Element) XMLTools.getChildByTagName(source, StaticStrings.LANGUAGE_METADATA_ELEMENT);
+		if (language_metadata != null)
+		{
+			String meta_name = language_metadata.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			Element language_meta = to.createElement(StaticStrings.LANGUAGE_METADATA_ELEMENT_STR);
+			if (meta_name.startsWith(StaticStrings.EXTRACTED_NAMESPACE) && meta_name.indexOf(StaticStrings.NS_SEP, StaticStrings.EXTRACTED_NAMESPACE.length()) == -1)
+			{
+				meta_name = meta_name.substring(StaticStrings.EXTRACTED_NAMESPACE.length());
+			}
+			language_meta.setAttribute(StaticStrings.NAME_ATTRIBUTE, meta_name);
+			search.appendChild(language_meta);
+		}
+	}
+
+	//convert indexes and their displayItems, which go in 'search' element in collectionConfig.xml
+	//parameter 'to' is the document to be saved as collectionConfig.xml
+	//parameter 'from' is the internal xml structure
+	static private void convertIndex(Document from, Document to)
+	{
+		Element source = from.getDocumentElement();
+		//Get the 'search' element from 'to' which has already been created in 'convertBuildType'
+		Element search = (Element) XMLTools.getChildByTagName(to.getDocumentElement(), StaticStrings.SEARCH_STR);
+
+		//THere are two sets of indexes elements, find the one which is assigned 'true'
+		Element indexes = XMLTools.getNamedElement(source, StaticStrings.INDEXES_ELEMENT, StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+		if (indexes == null)
+		{
+			return;
+		}
+		NodeList index_elements = indexes.getElementsByTagName(StaticStrings.INDEX_ELEMENT);
+		int index_elements_length = index_elements.getLength();
+
+		if (index_elements_length == 0)
+		{ // no indexes
+			return;
+		}
+
+		//find out it's mg or mgpp/lucene
+		String mg = search.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+		boolean mg_indexer = false;
+		if (mg.equals(StaticStrings.MG_STR))
+		{
+			mg_indexer = true;//it's mg, then the level is set as attribute of
+		}
+		if (mg_indexer == false)
+		{
+			// It's mgpp. Construct 'level' and 'defaultLevel' elements separately.
+			convertLevels(from, to, search);
+		}
+
+		for (int j = 0; j < index_elements_length; j++)
+		{
+			Element index_element = (Element) index_elements.item(j);
+			if (index_element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+			{
+				continue;
+			}
+
+			Element index_ele = to.createElement(StaticStrings.INDEX_LOW_STR);//index
+
+			// Used for creating displayItem for this element 'index_ele' further below
+			// full_index_names contain 'ex.'
+			String full_index_name = "";
+			String level_str = "";
+
+			StringBuffer index_value = new StringBuffer();
+			if (mg_indexer == true)
+			{
+				// For mg indexer, there is a 'level' attribute in the index element of the internal structure
+				// But mgpp/lucene don't
+				level_str = index_element.getAttribute(StaticStrings.LEVEL_ATTRIBUTE);
+				if (level_str.length() > 0)
+				{
+					index_value.append(level_str).append(StaticStrings.COLON_CHARACTER);
+					//index_value = index_value.StaticStrings.COLON_CHARACTER;
+				}
+			}
+
+			NodeList content_elements = index_element.getElementsByTagName(StaticStrings.CONTENT_ELEMENT);
+			int content_elements_length = content_elements.getLength();
+
+			for (int k = 0; k < content_elements_length; k++)
+			{
+				Element content_element = (Element) content_elements.item(k);
+				if (content_element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+				{
+					continue;
+				}
+				String name_str = content_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+
+				full_index_name = full_index_name + name_str;
+				if (k < content_elements_length - 1)
+				{
+					full_index_name = full_index_name + StaticStrings.COMMA_CHARACTER;
+				}
+
+				if (name_str.startsWith(StaticStrings.EXTRACTED_NAMESPACE) && name_str.indexOf(StaticStrings.NS_SEP, StaticStrings.EXTRACTED_NAMESPACE.length()) == -1)
+				{
+					name_str = name_str.substring(StaticStrings.EXTRACTED_NAMESPACE.length());
+				}
+
+				index_value.append(name_str);
+				name_str = null;
+				// Make it comma separated string
+				if (k < content_elements_length - 1)
+				{
+					index_value.append(StaticStrings.COMMA_CHARACTER);
+				}
+				content_element = null;
+			}//for loop ends
+
+			String temp_str = index_value.toString();
+			index_ele.setAttribute(StaticStrings.NAME_ATTRIBUTE, temp_str);
+
+			// Now constructing 'displayItem' element for this 'index_ele' element
+			// The index names in the collectionmetadata elements in the internal structure are not the names that
+			// are used in the content elements (i.e. ex.Source or dc.Subject and keywords), but the names that are
+			// in the configuration files (i.e. Source or dc.Subject)
+			ArrayList collectionmetadata_list = XMLTools.getNamedElementList(source, StaticStrings.COLLECTIONMETADATA_ELEMENT, StaticStrings.NAME_ATTRIBUTE, temp_str);
+
+			if (collectionmetadata_list == null)
+			{
+				//try the full name, i.e. with 'ex.'
+				if (mg_indexer == true)
+				{
+					// but first append level info if we are mg
+					full_index_name = level_str + StaticStrings.COLON_CHARACTER + full_index_name;
+				}
+				collectionmetadata_list = XMLTools.getNamedElementList(source, StaticStrings.COLLECTIONMETADATA_ELEMENT, StaticStrings.NAME_ATTRIBUTE, full_index_name);
+			}
+
+			if (collectionmetadata_list != null)
+			{
+
+				for (int k = 0; k < collectionmetadata_list.size(); k++)
+				{
+					Element collectionmetadata = (Element) collectionmetadata_list.get(k);
+					if (collectionmetadata.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+					{
+						continue;
+					}
+					Element displayItem = constructDisplayItem(collectionmetadata, to);
+
+					index_ele.appendChild(displayItem);
+				}
+			}
+
+			// deal with any <option name='' value=''> children of this index
+			// e.g. <option name='solrfieldtype' value='text_es'>
+			NodeList option_children = index_element.getElementsByTagName(StaticStrings.OPTION_ELEMENT);
+			for (int k = 0; k < option_children.getLength(); k++)
+			{
+				Element el = (Element) option_children.item(k);
+				if (el.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+				{
+					continue;
+				}
+				String name_str = el.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+				String value_str = XMLTools.getNodeText(el);
+
+				if (name_str == null && value_str == null)
+				{
+					continue;
+				}
+				Element option_element = to.createElement(StaticStrings.OPTION_STR);
+				if (!name_str.equals("")) {
+				    option_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+				}
+				if (!value_str.equals("")) { // or no value attribute without name attribute?
+				    option_element.setAttribute(StaticStrings.VALUE_ATTRIBUTE, value_str);	    
+				}
+				index_ele.appendChild(option_element);
+			}
+
+			search.appendChild(index_ele);
+
+		} //for loop ends
+
+		//Convert default index
+		convertDefaultIndex(from, to, search);
+		convertIndexOptions(from, to, search);
+	}
+
+    	//convert <facet> and <sort> and <defaultSort> elements 
+        //which go in 'search' element in collectionConfig.xml
+	//parameter 'to' is the document to be saved as collectionConfig.xml
+	//parameter 'from' is the internal xml structure
+  // for lucene and solr collections
+  // just a straight copy at present
+    // Note that GLI at present does not allow the user to set or modify the solr-specific elements 
+    // <sort> and <facet>. This function, and its inverse doSolrFacetsAndSorts, is only here to preserve such
+    // elements if they already exist in the collectionConfig.xml. At present they need to be manually added there.
+	static private void convertSolrFacetsAndSorts(Document from, Document to)
+	{
+		Element source = from.getDocumentElement();
+		//Get the 'search' element from 'to' which has already been created in 'convertBuildType'
+		Element search = (Element) XMLTools.getChildByTagName(to.getDocumentElement(), StaticStrings.SEARCH_STR);
+
+		//There are two sets of <Solr> elements, find the one which is assigned 'true'   ---  is this true???
+		Element solr = XMLTools.getNamedElement(source, StaticStrings.SOLR_ELEMENT, StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+		if (solr == null)
+		{
+		  return;
+		}
+		NodeList sort_elements = solr.getElementsByTagName(StaticStrings.SORT_LOW_STR);
+		int sort_elements_length = sort_elements.getLength();
+
+		for (int j = 0; j < sort_elements_length; j++)
+		{
+		  Element sort_ele = (Element) sort_elements.item(j);
+		  search.appendChild(to.importNode(sort_ele, true));
+		}
+		Element sort_def = (Element)XMLTools.getChildByTagName(solr, StaticStrings.SORT_DEFAULT_ELEMENT);
+		if (sort_def != null) {
+		  search.appendChild(to.importNode(sort_def, true));
+		}
+		NodeList facet_elements = solr.getElementsByTagName(StaticStrings.FACET_LOW_STR); 
+		int facet_elements_length = facet_elements.getLength();
+		for (int j = 0; j < facet_elements_length; j++)
+		  {
+		    Element facet_ele = (Element) facet_elements.item(j);
+		    search.appendChild(to.importNode(facet_ele, true));
+		  }
+
+	
+	}
+
+
+	// Convert levels for mgpp/lucene. This method is called by converIndex() when mgpp indexer is detected.
+	static private void convertLevels(Document from, Document to, Element search)
+	{
+		Element source = from.getDocumentElement();
+		Element index_option = XMLTools.getNamedElement(source, StaticStrings.INDEXOPTIONS_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.LEVELS_STR);
+		if (index_option == null)
+		{
+			return;
+		}
+		//Debugging purposes
+		if (index_option.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+		{
+			DebugStream.println("For mgpp, there should be an IndexOption element for levels which is assigned 'true': possible bug.");
+		}
+
+		NodeList option_elements = index_option.getElementsByTagName(StaticStrings.OPTION_ELEMENT);
+		int num_elements = option_elements.getLength();
+
+		// Don't output anything if no indexes are set
+		if (num_elements == 0)
+		{
+			return;//
+		}
+
+		for (int k = 0; k < num_elements; k++)
+		{
+			Element e = (Element) option_elements.item(k);
+			String name_str = e.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			Element level_element = to.createElement(StaticStrings.LEVEL_ELEMENT);
+			level_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_str);
+
+			//Now construct displayItem for this level element from collectionmetadata
+			ArrayList collectionmetadata_list = XMLTools.getNamedElementList(source, StaticStrings.COLLECTIONMETADATA_ELEMENT, StaticStrings.NAME_ATTRIBUTE, name_str);
+
+			if (collectionmetadata_list != null)
+			{
+
+				for (int j = 0; j < collectionmetadata_list.size(); j++)
+				{
+					Element collectionmetadata = (Element) collectionmetadata_list.get(j);
+
+					Element displayItem = constructDisplayItem(collectionmetadata, to);
+					level_element.appendChild(displayItem);
+				}
+			}
+			search.appendChild(level_element);
+		}
+
+		//Convert default level
+		Element default_index_option = XMLTools.getNamedElement(source, StaticStrings.INDEXOPTION_DEFAULT_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.LEVEL_DEFAULT_STR);
+		if (default_index_option == null)
+		{
+			return;
+		}
+		Element default_level = to.createElement(StaticStrings.LEVEL_DEFAULT_ELEMENT);
+		String default_level_str = default_index_option.getAttribute(StaticStrings.VALUE_ATTRIBUTE);
+		default_level.setAttribute(StaticStrings.NAME_ATTRIBUTE, default_level_str);
+		search.appendChild(default_level);
+
+	}
+
+	// Convert indexoptions for mg/mgpp/lucene. This method is called by convertIndex().
+	static private void convertIndexOptions(Document from, Document to, Element search)
+	{
+		Element source = from.getDocumentElement();
+		Element index_option = XMLTools.getNamedElement(source, StaticStrings.INDEXOPTIONS_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.INDEXOPTIONS_STR);
+		if (index_option == null)
+		{
+			return;
+		}
+		//Debugging purposes
+		if (index_option.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR))
+		{
+			DebugStream.println("There should be an IndexOption element which is assigned 'true': possible bug.");
+
+			// for lucene and solr collections, don't write out stemming, casefolding and accentfolding indexOptions
+			// since they are not meant to be editable: stemming and casefolding are always on in lucene
+			String buildtype_value = search.getAttribute(StaticStrings.TYPE_ATTRIBUTE); //might be mg|mgpp|lucene|solr
+			if(buildtype_value.equalsIgnoreCase("solr") || buildtype_value.equalsIgnoreCase("lucene")) {
+			    return;
+			}
+		} 
+		//Element indexOptionEl = to.createElement(StaticStrings.INDEXOPTION_STR);
+		NodeList option_elements = index_option.getElementsByTagName(StaticStrings.OPTION_ELEMENT);
+		int num_elements = option_elements.getLength();
+		// Don't output anything if no index
+		if (num_elements == 0)
+		{
+			return;//
+		}
+		//search.appendChild(indexOptionEl);
+
+		for (int k = 0; k < num_elements; k++)
+		{
+			Element e = (Element) option_elements.item(k);
+			String name_att = e.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			Element optionEl = to.createElement(StaticStrings.INDEXOPTION_STR);
+			optionEl.setAttribute(StaticStrings.NAME_ATTRIBUTE, name_att);
+			// default value?? on/off
+			//indexOptionEl.appendChild(optionEl);
+			search.appendChild(optionEl);
+		}
+
+	}
+
+	// handle top level elements which GLI knows nothing about
+	// we store them internally in a Unknown element for easy access when
+	// we write them out.
+	static private void doUnknownElements(Document to, Element from)
+	{
+		Element toElement = to.getDocumentElement();
+		Element unknownElement = to.createElement(StaticStrings.UNKNOWN_ELEMENT);
+		toElement.appendChild(unknownElement);
+
+		Node child = from.getFirstChild();
+		while (child != null)
+		{
+			if (child.getNodeType() == Node.ELEMENT_NODE && !known_element_names.contains(child.getNodeName()))
+			{
+				unknownElement.appendChild(XMLTools.duplicateElement(to, (Element) child, true));
+			}
+			child = child.getNextSibling();
+		}
+
+	}
+
+	// just copy all children of Unknown element into output doc.
+	static private void convertUnknownElements(Document from, Document to)
+	{
+
+		Element toElement = to.getDocumentElement();
+		Node unknown_element = XMLTools.getChildByTagName(from.getDocumentElement(), StaticStrings.UNKNOWN_ELEMENT);
+
+		Node child = unknown_element.getFirstChild();
+		while (child != null)
+		{
+			Element to_element = XMLTools.duplicateElement(to, (Element) child, true);
+			toElement.appendChild(to_element);
+
+			child = child.getNextSibling();
+		}
+
+	}
+
+	// Append the element son to the element mother in the appropriate position.
+	static public void appendProperly(Element mother, Element son)
+	{
+		if (son == null)
+			return;
+
+		Node reference_node = findInsertionPoint(mother, son);
+		if (reference_node != null)
+		{
+			mother.insertBefore(son, reference_node);
+		}
+		else
+		{
+			mother.appendChild(son);
+		}
+	}
+
+	/**
+	 * Find the best insertion position for the given DOM Element
+	 * 'target_element' in the DOM Element 'document_element'. This should try
+	 * to match command tag, and if found should then try to group by name or
+	 * type (eg CollectionMeta), or append to end is no such grouping exists (eg
+	 * Plugins). Failing a command match it will check against the command order
+	 * for the best insertion location.
+	 * 
+	 * @param target_element
+	 *            the command Element to be inserted
+	 * @return the Element which the given command should be inserted before, or
+	 *         null to append to end of list
+	 */
+	static public Node findInsertionPoint(Element document_element, Element target_element)
+	{
+		///ystem.err.println("Find insertion point: " + target_element.getNodeName());
+		String target_element_name = target_element.getNodeName();
+
+		// Try to find commands with the same tag.
+		NodeList matching_elements = document_element.getElementsByTagName(target_element_name);
+		// If we found matching elements, then we have our most likely insertion location, so check within for groupings
+		if (matching_elements.getLength() != 0)
+		{
+			///ystem.err.println("Found matching elements.");
+			// Only CollectionMeta are grouped.
+			if (target_element_name.equals(StaticStrings.COLLECTIONMETADATA_ELEMENT))
+			{
+				///ystem.err.println("Dealing with collection metadata");
+				// Special case: CollectionMeta can be added at either the start or end of a collection configuration file. However the start position is reserved for special metadata, so if no non-special metadata can be found we must append to the end.
+				// So if the command to be added is special add it immediately after any other special command
+				if (target_element.getAttribute(StaticStrings.SPECIAL_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
+				{
+					int index = 0;
+					Element matched_element = (Element) matching_elements.item(index);
+					Element sibling_element = (Element) matched_element.getNextSibling();
+					while (sibling_element.getAttribute(StaticStrings.SPECIAL_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
+					{
+						index++;
+						matched_element = (Element) matching_elements.item(index);
+						sibling_element = (Element) matched_element.getNextSibling();
+					}
+					if (sibling_element.getNodeName().equals(CollectionConfiguration.NEWLINE_ELEMENT))
+					{
+						Element newline_element = document_element.getOwnerDocument().createElement(CollectionConfiguration.NEWLINE_ELEMENT);
+						document_element.insertBefore(newline_element, sibling_element);
+					}
+					return sibling_element;
+				}
+				// Otherwise try to find a matching 'name' and add after the last one in that group.
+				else
+				{
+					int index = 0;
+					target_element_name = target_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+					boolean found = false;
+					// Skip all of the special metadata
+					Element matched_element = (Element) matching_elements.item(index);
+					while (matched_element.getAttribute(StaticStrings.SPECIAL_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
+					{
+						index++;
+						matched_element = (Element) matching_elements.item(index);
+					}
+					// Begin search
+					while (!found && matched_element != null)
+					{
+						if (matched_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equals(target_element_name))
+						{
+							found = true;
+						}
+						else
+						{
+							index++;
+							matched_element = (Element) matching_elements.item(index);
+						}
+					}
+					// If we found a match, we need to continue checking until we find the last name match.
+					if (found)
+					{
+						index++;
+						Element previous_sibling = matched_element;
+						Element sibling_element = (Element) matching_elements.item(index);
+						while (sibling_element != null && sibling_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equals(target_element_name))
+						{
+							previous_sibling = sibling_element;
+							index++;
+							sibling_element = (Element) matching_elements.item(index);
+						}
+						// Previous sibling now holds the command immediately before where we want to add, so find its next sibling and add to that. In this one case we can ignore new lines!
+						return previous_sibling.getNextSibling();
+					}
+					// If not found we just add after last metadata element
+					else
+					{
+						Element last_element = (Element) matching_elements.item(matching_elements.getLength() - 1);
+						return last_element.getNextSibling();
+					}
+				}
+
+			}
+			else
+			{
+				///ystem.err.println("Not dealing with collection meta.");
+				Element matched_element = (Element) matching_elements.item(matching_elements.getLength() - 1);
+				// One final quick test. If the matched element is immediately followed by a NewLine command, then we insert another NewLine after the matched command, then return the NewLine instead (thus the about to be inserted command will be placed between the two NewLines)
+				Node sibling_element = matched_element.getNextSibling();
+				if (sibling_element != null && sibling_element.getNodeName().equals(CollectionConfiguration.NEWLINE_ELEMENT))
+				{
+					Element newline_element = document_element.getOwnerDocument().createElement(CollectionConfiguration.NEWLINE_ELEMENT);
+					document_element.insertBefore(newline_element, sibling_element);
+				}
+				return sibling_element; // Note that this may be null
+			}
+		}
+		///ystem.err.println("No matching elements found.");
+		// Locate where this command is in the ordering
+		int command_index = -1;
+		for (int i = 0; command_index == -1 && i < CollectionConfiguration.COMMAND_ORDER.length; i++)
+		{
+			if (CollectionConfiguration.COMMAND_ORDER[i].equals(target_element_name))
+			{
+				command_index = i;
+			}
+		}
+		///ystem.err.println("Command index is: " + command_index);
+		// Now move forward, checking for existing elements in each of the preceeding command orders.
+		int preceeding_index = command_index - 1;
+		///ystem.err.println("Searching before the target command.");
+		while (preceeding_index >= 0)
+		{
+			matching_elements = document_element.getElementsByTagName(CollectionConfiguration.COMMAND_ORDER[preceeding_index]);
+			// If we've found a match
+			if (matching_elements.getLength() > 0)
+			{
+				// We add after the last element
+				Element matched_element = (Element) matching_elements.item(matching_elements.getLength() - 1);
+				// One final quick test. If the matched element is immediately followed by a NewLine command, then we insert another NewLine after the matched command, then return the NewLine instead (thus the about to be inserted command will be placed between the two NewLines)
+				Node sibling_element = matched_element.getNextSibling();
+				if (sibling_element != null && sibling_element.getNodeName().equals(CollectionConfiguration.NEWLINE_ELEMENT))
+				{
+					Element newline_element = document_element.getOwnerDocument().createElement(CollectionConfiguration.NEWLINE_ELEMENT);
+					document_element.insertBefore(newline_element, sibling_element);
+				}
+				return sibling_element; // Note that this may be null
+			}
+			preceeding_index--;
+		}
+		// If all that fails, we now move backwards through the commands
+		int susceeding_index = command_index + 1;
+		///ystem.err.println("Searching after the target command.");
+		while (susceeding_index < CollectionConfiguration.COMMAND_ORDER.length)
+		{
+			matching_elements = document_element.getElementsByTagName(CollectionConfiguration.COMMAND_ORDER[susceeding_index]);
+			// If we've found a match
+			if (matching_elements.getLength() > 0)
+			{
+				// We add before the first element
+				Element matched_element = (Element) matching_elements.item(0);
+				// One final quick test. If the matched element is immediately preceeded by a NewLine command, then we insert another NewLine before the matched command, then return this new NewLine instead (thus the about to be inserted command will be placed between the two NewLines)
+				Node sibling_element = matched_element.getPreviousSibling();
+				if (sibling_element != null && sibling_element.getNodeName().equals(CollectionConfiguration.NEWLINE_ELEMENT))
+				{
+					Element newline_element = document_element.getOwnerDocument().createElement(CollectionConfiguration.NEWLINE_ELEMENT);
+					document_element.insertBefore(newline_element, sibling_element);
+				}
+				return sibling_element; // Note that this may be null
+			}
+			susceeding_index++;
+		}
+		// Well. Apparently there are no other commands in this collection configuration. So append away...
+		return null;
+	}
+
+	// From collectionConfig.xml to internal structure:add 'ex.' namespace (if none).
+	// From internal structure to collectionConfig.xml:always peel off 'ex.' namespace (if any), except for format statement
+	//This method parses 'xml_file_doc' into 'dOc'
+	static public void parse(File xml_file, Document dOc)
+	{
+
+		Document xml_file_doc = XMLTools.parseXMLFile(xml_file);
+		Element fromElement = xml_file_doc.getDocumentElement();
+		Element toElement = dOc.getDocumentElement();
+
+		// security element. For now, we just save as is, as you can't edit this in GLI.
+		Node securityNode = XMLTools.getChildByTagNameIndexed(fromElement, StaticStrings.SECURITY_STR,0);
+		// just save for now
+		if (securityNode != null) {
+		  Element to_element = XMLTools.duplicateElement(dOc, (Element) securityNode, true);
+		  toElement.appendChild(to_element);
+		}
+		// It's deliberately set that 'creator', 'maintainer', and 'public' are only in English (as they are just names).
+		// So the following ArrayList have only one element.
+		Node metadataListNode = XMLTools.getChildByTagNameIndexed(fromElement, StaticStrings.METADATALIST_STR, 0);
+		if (metadataListNode != null)
+		{
+			ArrayList creator = doMetadataList(dOc, metadataListNode, StaticStrings.COLLECTIONMETADATA_CREATOR_ELEMENT, StaticStrings.COLLECTIONMETADATA_CREATOR_STR);
+			ArrayList maintainer = doMetadataList(dOc, metadataListNode, StaticStrings.COLLECTIONMETADATA_MAINTAINER_ELEMENT, StaticStrings.COLLECTIONMETADATA_MAINTAINER_STR);
+			ArrayList is_public = doMetadataList(dOc, metadataListNode, StaticStrings.COLLECTIONMETADATA_PUBLIC_ELEMENT, StaticStrings.COLLECTIONMETADATA_PUBLIC_STR);
+
+			appendArrayList(toElement, creator);
+			appendArrayList(toElement, maintainer);
+			appendArrayList(toElement, is_public);
+		}
+
+		Node databaseNode = XMLTools.getChildByTagNameIndexed(fromElement, StaticStrings.INFODB_STR, 0);
+		String databasetype_value = "gdbm";
+		if (databaseNode != null)
+		{
+			databasetype_value = ((Element) databaseNode).getAttribute(StaticStrings.TYPE_ATTRIBUTE);//might be gdbm|jdbm|sqlite OR not yet set (in which case it should default to gdbm)
+		}
+
+		Element databasetype = doDatabaseType(dOc, databasetype_value);
+		appendProperly(toElement, databasetype);
+
+		Node searchNode = XMLTools.getChildByTagNameIndexed(fromElement, StaticStrings.SEARCH_STR, 0);
+		String buildtype_value = ((Element) searchNode).getAttribute(StaticStrings.TYPE_ATTRIBUTE);//might be mg|mgpp|lucene
+		Element buildtype = doBuildType(dOc, buildtype_value);
+		appendProperly(toElement, buildtype);
+
+		Node importNode = XMLTools.getChildByTagNameIndexed(fromElement, StaticStrings.IMPORT_STR, 0);
+		if (importNode == null)
+		{
+			System.out.println("There is no content in the 'import' block.");
+		}
+		if (importNode != null)
+		{
+			//do plugin list nodes
+			Node pluginListNode = XMLTools.getChildByTagNameIndexed((Element) importNode, StaticStrings.PLUGINLIST_STR, 0);
+			if (pluginListNode == null)
+			{
+				System.out.println("There is no pluginlist set.");
+			}
+			if (pluginListNode != null)
+			{
+
+				doPlugins(dOc, pluginListNode);
+			}
+
+			//do the plugout element (used by building flax collections)
+			Node plugout = XMLTools.getChildByTagNameIndexed((Element) importNode, PLUGOUT_ELEMENT, 0);
+			if (plugout != null)
+			{
+				Element to_element = XMLTools.duplicateElement(dOc, (Element) plugout, true);
+				toElement.appendChild(to_element);
+			}
+		}
+
+		Node browseNode = XMLTools.getChildByTagNameIndexed(fromElement, StaticStrings.BROWSE_STR, 0);
+		if (browseNode != null)
+		{
+			if (browseNode == null)
+			{
+				System.out.println("There is no classifier.");
+			}
+			doClassifier(dOc, browseNode);
+		}
+
+		Node displayItemListNode = XMLTools.getChildByTagNameIndexed(fromElement, StaticStrings.DISPLAYITEMLIST_STR, 0);
+		if (displayItemListNode != null)
+		{
+			ArrayList description = doDisplayItemList(dOc, displayItemListNode, StaticStrings.DESCRIPTION_STR, StaticStrings.COLLECTIONMETADATA_COLLECTIONEXTRA_STR);
+			ArrayList smallicon = doDisplayItemList(dOc, displayItemListNode, StaticStrings.SMALLICON_STR, StaticStrings.COLLECTIONMETADATA_ICONCOLLECTIONSMALL_STR);
+			ArrayList icon = doDisplayItemList(dOc, displayItemListNode, StaticStrings.ICON_STR, StaticStrings.COLLECTIONMETADATA_ICONCOLLECTION_STR);
+			ArrayList name = doDisplayItemList(dOc, displayItemListNode, StaticStrings.NAME_STR, StaticStrings.COLLECTIONMETADATA_COLLECTIONNAME_STR);
+
+			appendArrayList(toElement, description);
+			appendArrayList(toElement, smallicon);
+			appendArrayList(toElement, icon);
+			appendArrayList(toElement, name);
+		}
+
+		if (buildtype_value.equalsIgnoreCase("mg"))
+		{
+			doMGIndexes(dOc, searchNode);
+		}
+		else
+		{
+			doMGPPIndexes(dOc, searchNode);
+		}
+
+		if(buildtype_value.equalsIgnoreCase("solr") || buildtype_value.equalsIgnoreCase("lucene")) {
+		    doSolrFacetsAndSorts(dOc, searchNode); // <facet><displayItem /></facet> and <sort><displayItem /></sort>
+		    // lucene will only have sort elements
+		}
+
+		doDefaultIndex(dOc, searchNode);
+		doDefaultLevel(dOc, searchNode);
+		doLevel(dOc, searchNode);
+		doIndexOption(dOc, searchNode);
+		doSubcollection(dOc, searchNode);
+		doIndexSubcollection(dOc, searchNode);
+		doIndexLanguage(dOc, searchNode);
+		doDefaultIndexLanguage(dOc, searchNode);
+		doLanguageMetadata(dOc, searchNode);
+		doSearchType(dOc, searchNode);
+		doSearchFormat(dOc, searchNode);
+		doGlobalFormat(dOc, fromElement);
+		doDisplayFormat(dOc, fromElement);
+		doReplaceListRef(dOc, fromElement);
+		doReplaceList(dOc, fromElement);
+		doServiceRackList(dOc, fromElement);
+		doUnknownElements(dOc, fromElement);
+		// the official displayItems in the displayItemList element have already been handled above
+		// and created as collectionmetadata elements in the dOc object
+		// Now we add in all the *other* (remaining) displayItems as collectionmeta elements
+		NodeList collectionMetadataList = dOc.getDocumentElement().getElementsByTagName(StaticStrings.COLLECTIONMETADATA_ELEMENT);
+		Set setOfUniqueColMetaNames = new HashSet();
+		for (int i = 0; i < collectionMetadataList.getLength(); i++)
+		{
+			Element colMeta = (Element) collectionMetadataList.item(i);
+			String name = colMeta.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			setOfUniqueColMetaNames.add(name);
+		}
+
+		if (displayItemListNode != null)
+		{
+			NodeList nl = ((Element) displayItemListNode).getElementsByTagName(StaticStrings.DISPLAYITEM_STR);
+
+			// make a list of all unique attribute names that are specifically not
+			// description, smallicon, icon and name, since these are already processed above
+			Set setOfUniqueDisplayItemNames = new LinkedHashSet();
+			for (int i = 0; i < nl.getLength(); i++)
+			{
+				Element displayItem = (Element) nl.item(i);
+				String name = displayItem.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+
+				if (name.equals(""))
+					continue; // no name attribute
+				if (setOfUniqueColMetaNames.contains(name))
+					continue;
+
+				if (name.equals(StaticStrings.DESCRIPTION_STR))
+					continue;
+				if (name.equals(StaticStrings.SMALLICON_STR))
+					continue;
+				if (name.equals(StaticStrings.ICON_STR))
+					continue;
+				if (name.equals(StaticStrings.NAME_STR))
+					continue;
+				// don't add displayItems that are handled by the indexers, etc. E.g. document:ex.Title 
+				if (name.indexOf(":") != -1)
+					continue;
+
+				// otherwise
+				setOfUniqueDisplayItemNames.add(name); // Set will ensure no duplicate names
+			}
+
+			Iterator i = setOfUniqueDisplayItemNames.iterator();
+			while (i.hasNext())
+			{
+				String displayItemName = (String) i.next();
+
+				ArrayList custom_displayItem = doDisplayItemList(dOc, displayItemListNode, displayItemName, displayItemName);
+				appendArrayList(toElement, custom_displayItem);
+			}
+		}
+
+	}
+
+	static public String generateStringVersion(Document doc)
+	{
+		return XMLTools.xmlNodeToString(doc);
+	}
+
+	static public void save(File collect_config_xml_file, Document doc)
+	{
+		Document collection_config_xml_document = convertInternalToCollectionConfig(doc);
+		String[] nonEscapingTagNames = { StaticStrings.FORMAT_STR, StaticStrings.DISPLAYITEM_STR };
+		XMLTools.writeXMLFile(collect_config_xml_file, collection_config_xml_document, nonEscapingTagNames);
+	}
+
+	//Convert the internal XML DOM tree (dOc) into that of collectionConfig.xml (skeleton)
+	static private Document convertInternalToCollectionConfig(Document dOc)
+	{
+		//first parse an empty skeleton of xml config file
+		//The aim is to convert the internal structure into this skeleton
+		// This skeleton just has the CollectionConfig element and nothing else
+		Document skeleton = XMLTools.parseXMLFile("xml/CollectionConfig.xml", true);
+		//Element internal = dOc.getDocumentElement();
+		convertSecurity(dOc, skeleton);
+		convertMetadataList(dOc, skeleton);
+		convertDisplayItemList(dOc, skeleton);
+		convertGlobalFormat(dOc, skeleton);
+		convertBuildType(dOc, skeleton);
+		convertDatabaseType(dOc, skeleton);
+		convertIndex(dOc, skeleton);
+		convertSolrFacetsAndSorts(dOc, skeleton);
+		convertPlugins(dOc, skeleton);//also do the plugout element
+		convertClassifier(dOc, skeleton);
+		convertSubcollectionIndexes(dOc, skeleton);
+		convertLanguages(dOc, skeleton);
+		convertSubcollection(dOc, skeleton);
+		convertSearchType(dOc, skeleton);
+		convertSearchFormat(dOc, skeleton);
+		convertDisplayFormat(dOc, skeleton);
+		convertReplaceListRef(dOc, skeleton);
+		convertReplaceList(dOc, skeleton);
+		convertServiceRackList(dOc, skeleton);
+		convertUnknownElements(dOc, skeleton); // try to catch everything GLI doesn't know about
+
+		return skeleton;
+	}
+
+	// Append the elements, which are of Element type, in 'list' to Element 'to'
+	static private void appendArrayList(Element to, ArrayList list)
+	{
+		if (list == null)
+			return;
+
+		for (int i = 0; i < list.size(); i++)
+		{
+			appendProperly(to, (Element) list.get(i));
+		}
+	}
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionConfiguration.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionConfiguration.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionConfiguration.java	(revision 31635)
@@ -0,0 +1,573 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.greenstone.LocalLibraryServer;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.DOMTree;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.greenstone.gatherer.util.Utility;
+import org.w3c.dom.*;
+
+/**
+ * This class provides access to an xml-type view of the collect.cfg file. This
+ * is useful as it allows the manipulation and free form editing of a
+ * collect.cfg file while still allowing the various CDM data managers to base
+ * themselves directly on this model (whereas they used to be independant
+ * ListModels which clobbered the ordering of unparsed commands).
+ * 
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ */
+public class CollectionConfiguration
+{
+	static final public String ENCODING = "UTF-8";
+	static final public String NEWLINE_ELEMENT = "NewLine";
+
+	static private Document document;
+	static private String saved_config_file_string = null;
+
+	// may be collec.cfg (GS2) or collectionConfig.xml (GS3)
+	private File collect_config_file;
+	private String collect_config_filename;
+
+	// This method is initilised in CollectionDesignManager.java constructor
+	public CollectionConfiguration(File collect_config_file)
+	{
+		this.collect_config_file = collect_config_file;
+		this.collect_config_filename = collect_config_file.getName();
+		// parse the XML template
+		document = XMLTools.parseXMLFile("xml/CollectionConfig.xml", true);
+		String filename = collect_config_filename.toLowerCase();
+
+		if (filename.endsWith(".cfg"))
+		{
+			saved_config_file_string = CollectCfgReadWrite.parse(collect_config_file, document);
+		}
+		else if (filename.endsWith(".xml"))
+		{
+			CollectionConfigXMLReadWrite.parse(collect_config_file, document);
+		}
+
+		//XMLTools.printXMLNode(document.getDocumentElement());
+	}
+
+	static public Element createElement(String element_name)
+	{
+		return document.createElement(element_name);
+	}
+
+	/** Gives the preferred ordering of commands */
+	static final public String[] COMMAND_ORDER = { StaticStrings.COLLECTIONMETADATA_CREATOR_ELEMENT, StaticStrings.COLLECTIONMETADATA_MAINTAINER_ELEMENT, StaticStrings.COLLECTIONMETADATA_PUBLIC_ELEMENT, StaticStrings.BUILDTYPE_ELEMENT, StaticStrings.PLUGIN_ELEMENT, StaticStrings.INDEXES_ELEMENT, StaticStrings.INDEX_DEFAULT_ELEMENT, StaticStrings.INDEXOPTIONS_ELEMENT, StaticStrings.INDEXOPTION_DEFAULT_ELEMENT, StaticStrings.LANGUAGES_ELEMENT, StaticStrings.LANGUAGE_DEFAULT_ELEMENT, StaticStrings.LANGUAGE_METADATA_ELEMENT, StaticStrings.SUBCOLLECTION_ELEMENT, StaticStrings.SUBCOLLECTION_INDEXES_ELEMENT, StaticStrings.SUBCOLLECTION_DEFAULT_INDEX_ELEMENT, StaticStrings.SUPERCOLLECTION_ELEMENT, StaticStrings.CLASSIFY_ELEMENT, StaticStrings.FORMAT_ELEMENT, StaticStrings.COLLECTIONMETADATA_ELEMENT };
+
+	/**
+	 * Find the best insertion position for the given DOM Element. This should
+	 * try to match command tag, and if found should then try to group by name
+	 * or type (eg CollectionMeta), or append to end is no such grouping exists
+	 * (eg Plugins). Failing a command match it will check against the command
+	 * order for the best insertion location.
+	 * 
+	 * @param target_element
+	 *            the command Element to be inserted
+	 * @return the Element which the given command should be inserted before, or
+	 *         null to append to end of list
+	 */
+	static public Node findInsertionPoint(Element target_element)
+	{
+		///ystem.err.println("Find insertion point: " + target_element.getNodeName());
+		String target_element_name = target_element.getNodeName();
+		Element document_element = document.getDocumentElement();
+		// Try to find commands with the same tag.
+		NodeList matching_elements = document_element.getElementsByTagName(target_element_name);
+		// If we found matching elements, then we have our most likely insertion location, so check within for groupings
+		if (matching_elements.getLength() != 0)
+		{
+			///ystem.err.println("Found matching elements.");
+			// Only CollectionMeta are grouped.
+			if (target_element_name.equals(StaticStrings.COLLECTIONMETADATA_ELEMENT))
+			{
+				///ystem.err.println("Dealing with collection metadata");
+				// Special case: CollectionMeta can be added at either the start or end of a collection configuration file. However the start position is reserved for special metadata, so if no non-special metadata can be found we must append to the end.
+				// So if the command to be added is special add it immediately after any other special command
+				if (target_element.getAttribute(StaticStrings.SPECIAL_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
+				{
+					int index = 0;
+					Element matched_element = (Element) matching_elements.item(index);
+					Element sibling_element = (Element) matched_element.getNextSibling();
+					while (sibling_element.getAttribute(StaticStrings.SPECIAL_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
+					{
+						index++;
+						matched_element = (Element) matching_elements.item(index);
+						sibling_element = (Element) matched_element.getNextSibling();
+					}
+					if (sibling_element.getNodeName().equals(NEWLINE_ELEMENT))
+					{
+						Element newline_element = document.createElement(NEWLINE_ELEMENT);
+						document_element.insertBefore(newline_element, sibling_element);
+					}
+					return sibling_element;
+				}
+				// Otherwise try to find a matching 'name' and add after the last one in that group.
+				else
+				{
+					int index = 0;
+					target_element_name = target_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+					boolean found = false;
+					// Skip all of the special metadata
+					Element matched_element = (Element) matching_elements.item(index);
+					while (matched_element.getAttribute(StaticStrings.SPECIAL_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
+					{
+						index++;
+						matched_element = (Element) matching_elements.item(index);
+					}
+					// Begin search
+					while (!found && matched_element != null)
+					{
+						if (matched_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equals(target_element_name))
+						{
+							found = true;
+						}
+						else
+						{
+							index++;
+							matched_element = (Element) matching_elements.item(index);
+						}
+					}
+					// If we found a match, we need to continue checking until we find the last name match.
+					if (found)
+					{
+						index++;
+						Element previous_sibling = matched_element;
+						Element sibling_element = (Element) matching_elements.item(index);
+						while (sibling_element != null && sibling_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equals(target_element_name))
+						{
+							previous_sibling = sibling_element;
+							index++;
+							sibling_element = (Element) matching_elements.item(index);
+						}
+						// Previous sibling now holds the command immediately before where we want to add, so find its next sibling and add to that. In this one case we can ignore new lines!
+						return previous_sibling.getNextSibling();
+					}
+					// If not found we just add after last metadata element
+					else
+					{
+						Element last_element = (Element) matching_elements.item(matching_elements.getLength() - 1);
+						return last_element.getNextSibling();
+					}
+				}
+
+			}
+			else
+			{
+				///ystem.err.println("Not dealing with collection meta.");
+				Element matched_element = (Element) matching_elements.item(matching_elements.getLength() - 1);
+				// One final quick test. If the matched element is immediately followed by a NewLine command, then we insert another NewLine after the matched command, then return the NewLine instead (thus the about to be inserted command will be placed between the two NewLines)
+				Node sibling_element = matched_element.getNextSibling();
+				if (sibling_element != null && sibling_element.getNodeName().equals(NEWLINE_ELEMENT))
+				{
+					Element newline_element = document.createElement(NEWLINE_ELEMENT);
+					document_element.insertBefore(newline_element, sibling_element);
+				}
+				return sibling_element; // Note that this may be null
+			}
+		}
+		///ystem.err.println("No matching elements found.");
+		// Locate where this command is in the ordering
+		int command_index = -1;
+		for (int i = 0; command_index == -1 && i < COMMAND_ORDER.length; i++)
+		{
+			if (COMMAND_ORDER[i].equals(target_element_name))
+			{
+				command_index = i;
+			}
+		}
+		///ystem.err.println("Command index is: " + command_index);
+		// Now move forward, checking for existing elements in each of the preceeding command orders.
+		int preceeding_index = command_index - 1;
+		///ystem.err.println("Searching before the target command.");
+		while (preceeding_index >= 0)
+		{
+			matching_elements = document_element.getElementsByTagName(COMMAND_ORDER[preceeding_index]);
+			// If we've found a match
+			if (matching_elements.getLength() > 0)
+			{
+				// We add after the last element
+				Element matched_element = (Element) matching_elements.item(matching_elements.getLength() - 1);
+				// One final quick test. If the matched element is immediately followed by a NewLine command, then we insert another NewLine after the matched command, then return the NewLine instead (thus the about to be inserted command will be placed between the two NewLines)
+				Node sibling_element = matched_element.getNextSibling();
+				if (sibling_element != null && sibling_element.getNodeName().equals(NEWLINE_ELEMENT))
+				{
+					Element newline_element = document.createElement(NEWLINE_ELEMENT);
+					document_element.insertBefore(newline_element, sibling_element);
+				}
+				return sibling_element; // Note that this may be null
+			}
+			preceeding_index--;
+		}
+		// If all that fails, we now move backwards through the commands
+		int susceeding_index = command_index + 1;
+		///ystem.err.println("Searching after the target command.");
+		while (susceeding_index < COMMAND_ORDER.length)
+		{
+			matching_elements = document_element.getElementsByTagName(COMMAND_ORDER[susceeding_index]);
+			// If we've found a match
+			if (matching_elements.getLength() > 0)
+			{
+				// We add before the first element
+				Element matched_element = (Element) matching_elements.item(0);
+				// One final quick test. If the matched element is immediately preceeded by a NewLine command, then we insert another NewLine before the matched command, then return this new NewLine instead (thus the about to be inserted command will be placed between the two NewLines)
+				Node sibling_element = matched_element.getPreviousSibling();
+				if (sibling_element != null && sibling_element.getNodeName().equals(NEWLINE_ELEMENT))
+				{
+					Element newline_element = document.createElement(NEWLINE_ELEMENT);
+					document_element.insertBefore(newline_element, sibling_element);
+				}
+				return sibling_element; // Note that this may be null
+			}
+			susceeding_index++;
+		}
+		// Well. Apparently there are no other commands in this collection configuration. So append away...
+		return null;
+	}
+
+	static public NodeList getElementsByTagName(String element_name)
+	{
+		return document.getDocumentElement().getElementsByTagName(element_name);
+	}
+
+	public Element getDocumentElement()
+	{
+		return document.getDocumentElement();
+	}
+
+	/**
+	 * This debug facility shows the currently loaded collect.cfg or
+	 * CollectConfig.xml file as a DOM tree.
+	 */
+	public void display()
+	{
+		JDialog dialog = new JDialog(Gatherer.g_man, "Collection Configuration", false);
+		dialog.setSize(400, 400);
+		JPanel content_pane = (JPanel) dialog.getContentPane();
+		final DOMTree tree = new DOMTree(document);
+		JButton refresh_button = new GLIButton("Refresh Tree");
+		refresh_button.addActionListener(new ActionListener()
+		{
+			public void actionPerformed(ActionEvent event)
+			{
+				tree.setDocument(document);
+			}
+		});
+		content_pane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+		content_pane.setLayout(new BorderLayout());
+		content_pane.add(new JScrollPane(tree), BorderLayout.CENTER);
+		content_pane.add(refresh_button, BorderLayout.SOUTH);
+		dialog.setVisible(true);
+	}
+
+	public File getFile()
+	{
+		return collect_config_file;
+	}
+
+	public Element getCreator()
+	{
+		Element element = getOrCreateElementByTagName(StaticStrings.COLLECTIONMETADATA_CREATOR_ELEMENT, null, null);
+		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.COLLECTIONMETADATA_CREATOR_STR);
+		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
+		return element;
+	}
+
+	public Element getMaintainer()
+	{
+		Element element = getOrCreateElementByTagName(StaticStrings.COLLECTIONMETADATA_MAINTAINER_ELEMENT, null, null);
+		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.COLLECTIONMETADATA_MAINTAINER_STR);
+		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
+		return element;
+	}
+
+	/** Retrieve or create the languages Element. */
+	public Element getLanguages()
+	{
+		return getOrCreateElementByTagName(StaticStrings.LANGUAGES_ELEMENT, null, null);
+	}
+
+	public Element getLanguageMetadata()
+	{
+		return getOrCreateElementByTagName(StaticStrings.LANGUAGE_METADATA_ELEMENT, null, null);
+	}
+
+	public Element getLevels()
+	{
+		return getOrCreateElementByTagName(StaticStrings.INDEXOPTIONS_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.LEVELS_STR);
+	}
+
+	public Element getLevelDefault()
+	{
+		return getOrCreateElementByTagName(StaticStrings.INDEXOPTION_DEFAULT_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.LEVEL_DEFAULT_STR);
+	}
+
+	public Element getIndexOptions()
+	{
+		return getOrCreateElementByTagName(StaticStrings.INDEXOPTIONS_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.INDEXOPTIONS_STR);
+	}
+
+	/**
+	 * Retrieve or create the indexes Element. Note that this method behaves
+	 * differently from the other getBlah methods, in that it also has to keep
+	 * in mind that indexes come in two flavours, MG and MGPP.
+	 */
+	public Element getMGIndexes()
+	{
+		return getOrCreateElementByTagName(StaticStrings.INDEXES_ELEMENT, StaticStrings.MGPP_ATTRIBUTE, StaticStrings.FALSE_STR);
+	}
+
+	public Element getMGPPIndexes()
+	{
+		return getOrCreateElementByTagName(StaticStrings.INDEXES_ELEMENT, StaticStrings.MGPP_ATTRIBUTE, StaticStrings.TRUE_STR);
+	}
+
+	public Element getPublic()
+	{
+		Element element = getOrCreateElementByTagName(StaticStrings.COLLECTIONMETADATA_PUBLIC_ELEMENT, null, null);
+		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.COLLECTIONMETADATA_PUBLIC_STR);
+		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
+		return element;
+	}
+
+	public Element getBuildType()
+	{
+		Element element = getOrCreateElementByTagName(StaticStrings.BUILDTYPE_ELEMENT, null, null);
+		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.BUILDTYPE_STR);
+		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
+		return element;
+
+	}
+
+	public Element getDatabaseType()
+	{
+		Element element = getOrCreateElementByTagName(StaticStrings.DATABASETYPE_ELEMENT, null, null);
+		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.DATABASETYPE_STR);
+		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
+		return element;
+
+	}
+
+	/** Retrieve or create the subindexes Element. */
+	public Element getSubIndexes()
+	{
+		return getOrCreateElementByTagName(StaticStrings.SUBCOLLECTION_INDEXES_ELEMENT, null, null);
+	}
+
+	/** Retrieve or create the supercollections Element. */
+	public Element getSuperCollection()
+	{
+		return getOrCreateElementByTagName(StaticStrings.SUPERCOLLECTION_ELEMENT, null, null);
+	}
+
+	public boolean ready()
+	{
+		return document != null;
+	}
+
+	/** ************************** Private Methods ***************************/
+
+	/** Retrieve or create the indexes Element. */
+	static private Element getOrCreateElementByTagName(String name, String conditional_attribute, String required_value)
+	{
+		Element document_element = document.getDocumentElement();
+		NodeList elements = document_element.getElementsByTagName(name);
+		int elements_length = elements.getLength();
+		if (elements_length > 0)
+		{
+			if (conditional_attribute == null)
+			{
+				document_element = null;
+				return (Element) elements.item(0);
+			}
+			else
+			{
+				for (int i = 0; i < elements_length; i++)
+				{
+					Element element = (Element) elements.item(i);
+					if (element.getAttribute(conditional_attribute).equals(required_value))
+					{
+						document_element = null;
+						return element;
+					}
+					element = null;
+				}
+			}
+		}
+		// Create the element
+		Element element = document.createElement(name);
+		// If there was a property set it
+		if (conditional_attribute != null)
+		{
+			element.setAttribute(conditional_attribute, required_value);
+		}
+		Node target_node = findInsertionPoint(element);
+		if (target_node != null)
+		{
+			document_element.insertBefore(element, target_node);
+		}
+		else
+		{
+			document_element.appendChild(element);
+		}
+		document_element = null;
+		return element;
+	}
+
+	/**
+	 * Write the text to the buffer. This is used so we don't have to worry
+	 * about storing intermediate String values just so we can calaulate length
+	 * and offset.
+	 * 
+	 * @param writer
+	 *            the BufferedWriter to which the str will be written
+	 * @param str
+	 *            the String to be written
+	 */
+	private void write(BufferedWriter writer, String str) throws IOException
+	{
+		writer.write(str, 0, str.length());
+	}
+
+	public void saveIfNecessary()
+	{
+
+		// Generate a string version of internal document
+		String config_file_string = null;
+		if (Gatherer.GS3)
+		{
+			config_file_string = CollectionConfigXMLReadWrite.generateStringVersion(document);
+		}
+		else
+		{
+			config_file_string = CollectCfgReadWrite.generateStringVersion(document);
+		}
+		// compare to saved version
+		if (saved_config_file_string != null)
+		{
+			if (saved_config_file_string.equals(config_file_string))
+			{
+				DebugStream.println(collect_config_filename + " file hasn't changed so no save necessary...");
+				return;
+			}
+		}
+
+		// We need to save...
+		DebugStream.println(collect_config_filename + " file has changed, saving now...");
+
+		// If we're using the Local Library we must release the collection before writing to the collect.cfg file
+		String collection_name = CollectionManager.getLoadedCollectionName(true); // url style slash
+		boolean collection_released = false;
+		if (Gatherer.c_man.built() && LocalLibraryServer.isRunning() == true)
+		{
+			// Release the collection
+			LocalLibraryServer.releaseCollection(collection_name);
+			collection_released = true;
+		}
+
+		// Make a backup of the existing config file
+		if (collect_config_file.exists())
+		{
+			String config_filename;
+			String backup_filename;
+			if (Gatherer.GS3)
+			{
+				config_filename = Utility.COLLECTION_CONFIG_XML;
+				backup_filename = Utility.COLLECTION_CONFIG_BAK;
+			}
+			else
+			{
+				config_filename = StaticStrings.COLLECT_CFG;
+				backup_filename = Utility.COLLECT_BAK;
+			}
+			File original_file = new File(collect_config_file.getParentFile(), config_filename);
+			File backup_file = new File(collect_config_file.getParentFile(), backup_filename);
+			if (backup_file.exists())
+			{
+				backup_file.delete();
+			}
+			if (!original_file.renameTo(backup_file))
+			{
+				System.err.println("Warning: can't rename " + config_filename + " to " + backup_filename);
+			}
+		}
+
+		// now save the file
+		if (Gatherer.GS3)
+		{
+			CollectionConfigXMLReadWrite.save(collect_config_file, document);
+		}
+		else
+		{
+			// we have already converted to string, so save here
+			try
+			{
+				OutputStream ostream = new FileOutputStream(collect_config_file);
+				Writer file_writer = new OutputStreamWriter(ostream, ENCODING);
+				BufferedWriter buffered_writer = new BufferedWriter(file_writer);
+				buffered_writer.write(config_file_string);
+				buffered_writer.close();
+			}
+			catch (Exception exception)
+			{
+				DebugStream.println("Error in CollectionConfiguration.save(): " + exception);
+				DebugStream.printStackTrace(exception);
+			}
+
+		}
+
+		// save the string version
+		saved_config_file_string = config_file_string;
+
+		// If we're using a remote Greenstone server, upload the new collect.cfg file
+		if (Gatherer.isGsdlRemote)
+		{
+			Gatherer.remoteGreenstoneServer.uploadCollectionFile(collection_name, collect_config_file);
+		}
+
+		// Now re-add the collection to the Local Library server
+		if (collection_released)
+		{
+			LocalLibraryServer.addCollection(collection_name);
+		}
+	}
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionDesignManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionDesignManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionDesignManager.java	(revision 31635)
@@ -0,0 +1,276 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.event.*;
+import java.io.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.w3c.dom.*;
+
+//save()
+import org.greenstone.gatherer.util.XMLTools;
+import javax.swing.*;
+
+/** This manager provides access to submanagers, which in turn provide tools for the designing of Greenstone collections via the information stored in etc/collect.cfg. This class acts as a hub for the managers that handle specific parts of the configuration such as classifiers, format strings and language settings.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3d
+ */
+public class CollectionDesignManager {
+    /** This listener listens for any event on any of the components in any of the sub-views, and marks the collection as needing saving if any change occurs. */
+    static public CDMChangeListener change_listener;
+    /** These listeners listen to changes in the Design mode so as to allow minimal rebuilding */
+    static public DesignChangeListener all_change_listener;
+    static public DesignChangeListener buildcol_change_listener;
+    static public DesignChangeListener databasecol_change_listener;
+    /** A list of classifiers to use at build time. */
+    static public ClassifierManager classifier_manager;
+    /** The CollectionConfiguration object on which this CDM will be based. */
+    static public CollectionConfiguration collect_config;
+    /** A manager of collection level metadata. */
+    static public CollectionMetaManager collectionmeta_manager;
+    /** A list of formating strings to use at build time. */
+    static public SharedByTwoFormatManager format_manager;
+    /** A manager of general options */
+    static public GeneralManager general_manager;
+    /** List of indexes to be built, and the default index. 
+     also handles build type and levels */
+    static public IndexingManager index_manager;
+    /** Contains instructions dealing with the collection language. */
+    static public LanguageManager language_manager;
+    /** Handling writing extra.dm file */
+    static public MacrosManager macros_manager;
+    /** A list of plugins to use at build time. */
+    static public PluginManager plugin_manager;
+    /** a manager of searching metadata such as index names*/ 
+    static public SearchMetadataManager searchmetadata_manager;
+    /** Contains: A list of subcollections, (defined on metadatadata), a list of which subcollection indexes to build and the default subcollection index. */
+    static public SubcollectionManager subcollection_manager;
+
+    static public SubcollectionIndexManager subcollectionindex_manager;
+    /** A supercollection command allows a single search to be conducted across several collections. It is a very basic command and so avoids all the crazy model stuff that exists in most of the design managers. */
+    static public SuperCollectionManager supercollection_manager; // Just cause I could ;p
+    /** The text translation manager. */
+    static public TranslationView translation_manager;
+    /** A manager of configuring depositor metadata */ 
+    static public DepositorMetadataManager depositormetadata_manager;
+
+    /** These mark what needs to happen when building a collection where ONLY design options have been changed.
+        The build requirements of the higher numbers must include doing everything from the lower numbers. */
+    static final public int ALL = 3;
+    static final public int BUILDCOL = 2;
+    static final public int NOTHING = 0;
+    static private int rebuildTypeRequired = NOTHING; //Rebuild type required if only design options have changed
+
+    /** This indicates whether a minimal or complete build is required.
+        Minimal means do an incremental rebuild. Complete means do a full rebuild
+        Note: This stores preceisely whether the user selected the minimal or full build radio button.
+        Its value does not indicate anything about what gli eventually chose as a result of that "suggestion" */
+    static private boolean isCompleteBuild = false;
+
+    /** These remember which scripts were run for the last build.
+        true indicated 'full-' prefix, false indicates 'incremental-' prefix to the given perl script (import.pl or buildcol.pl)
+        Note: This stores what gli did, regardless of whether minimal or full was requested by the user */
+    static private boolean importWasFull = false;
+    static private boolean buildcolWasFull = false;
+
+    /** Constructor. Loads a certain collection configuration file, which is parsed into a DOM. This model is then registered with the command information managers, each of whom knows how to, and provides controls to, alter certain commands.
+     * @param collect_config_file the File representing a collection configuration file either in its text (G2) or xml (G3) form
+     */
+    public CollectionDesignManager(File collect_config_file) {
+	DebugStream.println("Initializaing CollectionDesignModule.");
+	change_listener = new CDMChangeListener();
+	all_change_listener = new DesignChangeListener(ALL);
+	buildcol_change_listener = new DesignChangeListener(BUILDCOL);
+	databasecol_change_listener = new DesignChangeListener(BUILDCOL);
+	
+	// Parse the collection configuration
+	collect_config = new CollectionConfiguration(collect_config_file);
+	if (DebugStream.isDebuggingEnabled()) {
+	    collect_config.display();
+	}
+	loadDesignDetails();
+	DebugStream.println("CollectionDesignModule loaded.");
+    }
+
+    /** Reloads the various managers to ensure they have built themselves from the latest details available in the collection configuration class
+     * @see org.greenstone.gatherer.cdm.ClassifierManager
+     * @see org.greenstone.gatherer.cdm.CollectionMetaManager
+     * @see org.greenstone.gatherer.cdm.FormatManager
+     * @see org.greenstone.gatherer.cdm.GeneralManager
+     * @see org.greenstone.gatherer.cdm.IndexManager
+     * @see org.greenstone.gatherer.cdm.LanguageManager
+     * @see org.greenstone.gatherer.cdm.PluginManager
+     * @see org.greenstone.gatherer.cdm.SubcollectionIndexManager
+     * @see org.greenstone.gatherer.cdm.SubcollectionManager
+     * @see org.greenstone.gatherer.cdm.SuperCollectionManager
+     * @see org.greenstone.gatherer.cdm.TranslationView
+     */
+    private void loadDesignDetails() {
+	// Create the command information managers, registering the config file with each as necessary
+	language_manager = new LanguageManager(collect_config.getLanguages());
+	collectionmeta_manager = new CollectionMetaManager();
+	classifier_manager = new ClassifierManager();
+	general_manager = new GeneralManager();
+	macros_manager = new MacrosManager();
+	index_manager = new IndexingManager();
+	plugin_manager = new PluginManager();
+	subcollection_manager = new SubcollectionManager();
+	subcollectionindex_manager = new SubcollectionIndexManager(collect_config.getSubIndexes());
+	supercollection_manager = new SuperCollectionManager(collect_config.getSuperCollection());
+	searchmetadata_manager = new SearchMetadataManager();
+	depositormetadata_manager = new DepositorMetadataManager();
+	translation_manager = new TranslationView();
+    if (Gatherer.GS3) {
+        format_manager = new Format4gs3Manager();
+    } else {
+        format_manager = new FormatManager(); // Parse formats at the very end, given that they depend upon several other managers to appear properly.
+    }
+    }
+
+    /** This method deconstructs each of the managers, causing them to dispose of their controls.
+     */
+    public void destroy() {
+	// Remove references from persistant listeners.
+	classifier_manager.destroy();
+	classifier_manager = null;
+	searchmetadata_manager.destroy();
+	searchmetadata_manager = null;
+	format_manager.destroy();
+	format_manager = null;
+//    format_4gs3_manager.destroy();
+//    format_4gs3_manager = null;
+	general_manager.destroy();
+	general_manager = null;
+	index_manager.destroy();
+	index_manager = null;
+	language_manager.destroy();
+	language_manager = null;
+	plugin_manager.destroy();
+	plugin_manager = null;
+	subcollection_manager.destroy();
+	subcollection_manager = null;
+	supercollection_manager.destroy();
+	supercollection_manager = null;
+	depositormetadata_manager.destroy();
+	depositormetadata_manager = null;
+	translation_manager.destroy();
+	translation_manager = null;
+    }
+
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+	plugin_manager.modeChanged(mode);
+	classifier_manager.modeChanged(mode);
+	subcollection_manager.modeChanged(mode);
+	supercollection_manager.modeChanged(mode);
+	//format_manager.modeChanged(mode);
+	index_manager.modeChanged(mode);
+	translation_manager.modeChanged(mode);
+	general_manager.modeChanged(mode);
+	language_manager.modeChanged(mode);
+	searchmetadata_manager.modeChanged(mode);
+	depositormetadata_manager.modeChanged(mode);
+    }
+
+    /** The cdm is considered to be ready if the collect.cfg file was found and parsed and the collection title is not error.
+     * @return true if the collection is ready, false otherwise
+     */
+    public boolean ready() {
+	return collect_config.ready();
+    }
+
+
+    /** Cause the current collection configuration to be written out to disk.
+     */
+    public void save() {
+      collect_config.saveIfNecessary();
+    }
+
+
+    public static int getRebuildTypeRequired() {
+        return rebuildTypeRequired;
+    }
+
+    public static void resetRebuildTypeRequired() {
+        setRebuildTypeRequired(NOTHING);
+    }
+    public static void setRebuildTypeRequired(int number) {
+        rebuildTypeRequired = number;
+    }
+
+    public static boolean isCompleteBuild() {
+        return isCompleteBuild;
+    }
+    public static void setCompleteBuild(boolean icb) {
+        isCompleteBuild = icb;
+    }
+
+	public static void setImportWasFull( boolean wasFull ) {
+		importWasFull = wasFull;
+	}
+	public static boolean importWasFull() {
+		return importWasFull;
+	}
+	public static void setBuildcolWasFull( boolean wasFull ) {
+		buildcolWasFull = wasFull;
+	}
+	public static boolean buildcolWasFull() {
+		return buildcolWasFull;
+	}
+
+    /**
+     * What exactly does this do?
+     */
+    private class CDMChangeListener
+	implements ActionListener, DocumentListener {
+
+	/** Gives notification that an event has happened */
+	public void actionPerformed(ActionEvent event) {
+	    Gatherer.c_man.getCollection().setSaved(false);
+	}
+
+	/** Gives notification that an attribute or set of attributes changed. */
+	public void changedUpdate(DocumentEvent e) {
+	    Gatherer.c_man.getCollection().setSaved(false);
+	}
+
+	/** Gives notification that there was an insert into the document. */
+	public void insertUpdate(DocumentEvent e) {
+	    Gatherer.c_man.getCollection().setSaved(false);
+	}
+
+	/** Gives notification that a portion of the document has been removed. */
+	public void removeUpdate(DocumentEvent e) {
+	    Gatherer.c_man.getCollection().setSaved(false);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionMeta.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionMeta.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionMeta.java	(revision 31635)
@@ -0,0 +1,188 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+
+/** This class encapsulates a single collection level metadata assignment, which constitutes a name, language and value.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */
+public class CollectionMeta
+    implements DOMProxyListEntry {
+
+    static final public boolean TEXT = true;
+    static final public boolean GREENSTONE = false;
+
+    private boolean dummy = false;
+    private Element element = null;
+    private String text = null;
+
+    /** Constructor.
+     * @param element the Element from which we will determine metadata details
+     */
+    public CollectionMeta(Element element) {
+	this.element = element;
+    }
+
+    /** Constructor to create a new piece of metadata given its name. */
+    public CollectionMeta(String name) {
+	element = CollectionConfiguration.createElement(StaticStrings.COLLECTIONMETADATA_ELEMENT);
+	element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+	element.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, Configuration.getLanguage());
+	element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+    }
+
+    /** Constructor to create a new piece of metadata given its name. */
+    public CollectionMeta(String name, String language) {
+	element = CollectionConfiguration.createElement(StaticStrings.COLLECTIONMETADATA_ELEMENT);
+	element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+	element.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, language);
+	element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+    }
+
+    /** Constructor to create a new piece of metadata given its name. */
+    public CollectionMeta(String name, String language, boolean dummy) {
+	this(name, language);
+	this.dummy = dummy;
+    }
+
+    /** Method to compare two collection metadata objects to calculate their respective ordering.
+     * @param object the other metadata to compare to, as an Object
+     * @return an int which is less than 0 if this object proceeds the given object, 0 if they are equal and greater than 0 otherwise.
+     * @see org.greenstone.gatherer.cdm.Language
+     */
+    public int compareTo(Object object) {
+	return toString().compareTo(object.toString());
+    }
+
+    /** Factory constructor. */
+    public DOMProxyListEntry create(Element element) {
+	return new CollectionMeta(element);
+    }
+
+    /** Method to compare two collection metadata objects for equality.
+     * @param object The other metadata to compare to, as an <strong>Object</strong>.
+     * @return A <i>boolean</i> value of <i>true</i> if the object are equal, <i>false</i> otherwise.
+     */
+    public boolean equals(Object object) {
+	return (compareTo(object) == 0);
+    }
+
+    public Element getElement() {
+	return element;
+    }
+
+    /** Method to retrieve the value of language.
+     * @return the value of language as a <strong>String</strong>.
+     */
+    public String getLanguage() {
+	// Retrieve the language string
+	return element.getAttribute(StaticStrings.LANGUAGE_ATTRIBUTE);
+    }
+
+    /** Method to retrieve the value of name.
+     * @return the name attribute of the collection meta as a String 
+     */
+    public String getName() {
+	return element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+    }
+    /** Method to retrieve the value of value (well great choice of name there).
+     * @return The value of value as a <strong>String</strong>.
+     */
+    public String getValue(boolean text_value) {
+	String raw_value = XMLTools.getValue(element);
+	// Decode the raw value depending on whether the user asked for the TEXT or GREENSTONE version
+	if(text_value == TEXT) {
+	    return Codec.transform(raw_value, Codec.DOM_TO_TEXT);
+	}
+	else {
+	    return Codec.transform(raw_value, Codec.DOM_TO_GREENSTONE);
+	}
+    }
+
+    public boolean isAssigned() {
+	return (element != null && !element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR));
+    }
+
+    public boolean isDummy() {
+	return dummy;
+    }
+
+    /** Determine if this metadata is one of the four special pieces of metadata.
+     * @return true if this metadata is special, false otherwise.
+     */
+    public boolean isSpecial() {
+	return (element != null && element.getAttribute(StaticStrings.SPECIAL_ATTRIBUTE).equals(StaticStrings.TRUE_STR));
+    }
+
+    public void setAssigned(boolean assigned) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	}
+    }
+
+    public void setElement(Element element) {
+	this.element = element;
+	text = null;
+    }
+
+    /** Change the value of value. 
+     * @param raw_value the new value as a String.
+     */
+    public void setValue(String raw_value) {
+	// we need to check if the value has changed
+	String current_value = XMLTools.getValue(element);
+	String new_value = Codec.transform(raw_value, Codec.TEXT_TO_DOM);
+	if (!current_value.equals(new_value)) {
+	// Only raw html text can be given to setValue so we need to encode it
+	    XMLTools.setValue(element, new_value);
+	    text = null; // Reset text
+	    // And determine if this makes the metadata assigned
+	    setAssigned(raw_value != null && raw_value.length() > 0);
+	}
+    }
+
+    /** Method to print out this class as it would appear within the collection configuration file.
+     * @return A <strong>String</strong> containing the text value of this class.
+     */
+    public String toString() {
+	if(text == null) {
+	  text = CollectCfgReadWrite.toString(element);
+	    if (text.equals("")){
+		text = getName();
+	    }
+	}
+	return text;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionMetaManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionMetaManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CollectionMetaManager.java	(revision 31635)
@@ -0,0 +1,200 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.*;
+
+/** This class is responsible for maintaining a list of assigned collection level metadata, and for allows manipulations on the aforementioned data.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3d
+ */
+public class CollectionMetaManager 
+    extends DOMProxyListModel {
+    
+    /** Constructor. */
+    public CollectionMetaManager() {
+	super(CollectionDesignManager.collect_config.getDocumentElement(), StaticStrings.COLLECTIONMETADATA_ELEMENT, new CollectionMeta(""));
+	DebugStream.println("CollectionMetaManager: " + getSize() + " metadata parsed.");
+    }
+    /** Method to add a new piece of metadata.
+     * @param metadata the new CollectionMeta
+     */
+    public void addMetadatum(CollectionMeta metadata) {
+	if(!contains(metadata)) {
+	    Element element = metadata.getElement();
+	    // Locate where we should insert this new metadata. 
+	    Node target_node = CollectionConfiguration.findInsertionPoint(element);
+	    add(root, metadata, target_node);
+	}
+    }
+
+    public CollectionMeta get(int i) {
+	return (CollectionMeta) getElementAt(i);
+    }
+
+    /** Retrieve the languages in use for the metadata assigned to this collection
+     * @return an TreeSet containing the languages used
+     */
+    public TreeSet getLanguages() {
+	TreeSet result = new TreeSet();
+	int size = getSize();
+	for(int i = 0; i < size; i++) {
+	    CollectionMeta metadata = (CollectionMeta) getElementAt(i);
+	    String language = metadata.getLanguage();
+	    result.add(language);
+	}
+	return result;
+    }
+
+    /** Retrieve all of the general metadata. */
+    public ArrayList getMetadata() {
+	ArrayList result = new ArrayList();
+	int size = getSize();
+	for(int i = 0; i < size; i++) {
+	    CollectionMeta metadata = (CollectionMeta) getElementAt(i);
+	    if(!metadata.getName().startsWith(StaticStrings.STOP_CHARACTER)) {
+		result.add(metadata);
+	    }
+	}
+	return result;
+    }
+	 
+    /** Retrieve all of the metadata for the given feature, regardless of language. */
+    public ArrayList getMetadata(String name) {
+	ArrayList result = new ArrayList();
+	int size = getSize(); // Refresh DOM Model
+	for(int i = 0; i < size; i++) {
+	    CollectionMeta metadata = (CollectionMeta) getElementAt(i);
+	    if(metadata.getName().equals(name)) {
+		result.add(metadata);
+	    }
+	}
+	return result;
+    }
+
+    /** Retrieve the named piece of metadata, in the default language, if available. If no such metadata is available then it is created.
+     * @param name the name of the metadatum to retrieve as a String
+     * @return the dom Element containing the specified metadatum
+     */
+    public CollectionMeta getMetadatum(String name) {
+	return getMetadatum(name, true);
+    }
+
+    public CollectionMeta getMetadatum(String name, boolean add_if_not_found) {
+	//DebugStream.println("Get the metadata for " + name + " in the default language.");
+	int size = getSize();
+	for(int i = 0; i < size; i++) {
+	    CollectionMeta metadatum = (CollectionMeta) getElementAt(i);
+	    if(metadatum.getName().equals(name) && metadatum.getLanguage().equals(Configuration.getLanguage())) {
+		DebugStream.println("Found '" + metadatum + "'");
+		return metadatum;
+	    }
+	    else {
+		//DebugStream.println("No match with: " + metadatum.getName() + " [l=" + metadatum.getLanguage() + "] \"" + metadatum.getValue() + "\"");
+	    }
+	    metadatum = null;
+	}
+	if(add_if_not_found) {
+	    CollectionMeta result = new CollectionMeta(name);
+	    addMetadatum(result);
+	    DebugStream.println("Added new metadata: " + name);
+	    return result;
+	}
+	else {
+	    return null;
+	}
+    }
+
+    /** Method to retrieve a certain piece of metadata based on its name and language.
+     * @param name the name of the metadata as an Object (as it may actually be a refernce to an Index or SubIndex)
+     * @param language the language of the metadata.
+     * @param partial <i>true</i> to return the first partial match (ie matches name but not language).
+     * @return The <strong>CollectionMeta</strong> requested, or <i>null</i> if no such metadata.
+     */
+    public CollectionMeta getMetadata(String name, String language, boolean partial) { 
+	CollectionMeta partial_match = null;
+	for(int i = 0; i < getSize(); i++) {
+	    CollectionMeta metadata = (CollectionMeta) getElementAt(i);
+	    Object metadata_name = metadata.getName();
+	    // We test the case of an object match (ie Index to Index)...
+	    if(metadata_name.equals(name)) {
+		if (metadata.getLanguage().equals(language)) {
+		    return metadata;
+		}
+		partial_match = metadata;
+	    }
+	}
+	if(partial) {
+	    return partial_match;
+	}
+	return null;
+    }
+
+    /** Method to remove a piece of metadata.
+     * @param metadata metadata
+     */
+    public void removeMetadata(CollectionMeta metadata) {
+	if(metadata != null) {
+	    String name = metadata.getName();
+	    String language = metadata.getLanguage();
+	    for(int i = 0; i < getSize(); i++) {
+		CollectionMeta other = (CollectionMeta) getElementAt(i);
+		if(name.equals(other.getName()) && language.equals(other.getLanguage())) {
+		    remove(i);
+		    return;
+		}
+		other = null;
+	    }	
+	    language = null;
+	    name = null;
+	}
+    }
+
+    /** Removes all of the metadata with a certain name, regardless of language or value. */
+    public void removeMetadata(String name) {
+	for(int i = getSize(); i != 0; i--) {
+	    CollectionMeta other = (CollectionMeta) getElementAt(i - 1);
+	    if(name.equals(other.getName())) {
+		remove(i - 1);
+	    }
+	    other = null;
+	}	
+    }
+
+    public int size() {
+	return getSize();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CommandTokenizer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CommandTokenizer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/CommandTokenizer.java	(revision 31635)
@@ -0,0 +1,225 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+/**************************************************************************************
+ * Title:		Gatherer
+ * Description:  The Gatherer: a tool for gathering and enriching a digital collection.
+ * Company:	  The University of Waikato
+ * Written:	  07/05/02
+ * Revised:	  03/10/02 - Commented
+ **************************************************************************************/
+import java.io.BufferedReader;
+import java.util.StringTokenizer;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.util.StaticStrings;
+
+/** This class provides an extension to the standard StringTokenizer in that it recognizes quotes (or some form of bracketting) enclose a single token so in something like: <br>format Search '&lt;table&gt;&lt;img src=... &lt;/table&gt;'<br> the formatting string is parsed as a single token. Unfortunately this makes countTokens() unreliable for exact measurement of tokens remaining, and only useful for determining if there are tokens left to be processed (includes any that have already been read into command buffer).
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class CommandTokenizer {
+    
+    static final public int BRACKET_ENCLOSED		= 0;
+    static final public int DOUBLE_QUOTE_ENCLOSED	= 1;
+    static final public int NORMAL			= 2;
+    static final public int QUOTE_ENCLOSED		= 3;
+    
+    private BufferedReader in_stream;
+    private int count = -1;
+    private StringTokenizer internal_tokenizer;
+    private boolean strip_quotes = true;
+    /** Basic Constructor. Used to parse tokens from a string keeping tokens surrounded by speechmarks or square brackets intact. Thus something like:<br>
+     * collectionmeta collectionextra [l = en] "Hello World"<br>
+     * is tokenized thus<br>
+     * {'collectionmeta', 'collectionextra', 'l = en', 'Hello World'}
+     * @param command the command String you wish to tokenize
+     */
+    public CommandTokenizer(String command) {
+	this.internal_tokenizer = new StringTokenizer(command);
+	this.in_stream = null;
+    }
+    
+    /** Advanced Constructor. As above but with one major difference. Since it is provided an input stream (presumably where the command string originated from), it is able to parse a quote enclosed command token that stretches over several lines. Each newline is preserved in the resulting token. There is an extra bitchslap here as comething like a collection extra might have html code in them that contain escaped speechmarks, so extra care must be taken not to break at them. Thus something like:<br>
+     * collectionmeta collectionextra [l = en] "<br>
+     *     an example of the crazy as description we sometimes get which includes of all things something like <a href=\"this.html\"<br>
+     *     >this</a> which you could easily see might be a problem if I parse this niavely."<br>
+     * is tokenized thus<br>
+     * {'collectionmeta', 'collectionextra', 'l = en', '\nan example of the crazy as description we sometimes get which includes of all things something like <a href=\"this.html\"\n>this</a> which you could easily see might be a problem if I parse this niavely.'}
+     * @param command the command String you wish to tokenize
+     * @param in_stream a BufferedReader from which the tokenizer can draw further lines as necessary
+     */
+    public CommandTokenizer(String command, BufferedReader in_stream) {
+	///atherer.println("***** CommandTokenizer *****\nparse:\t" + command + "\n****************************");
+	this.internal_tokenizer = new StringTokenizer(command);
+	this.in_stream = in_stream;
+    }
+    
+    public CommandTokenizer(String command, BufferedReader in_stream, boolean strip_quotes) {
+	this.internal_tokenizer = new StringTokenizer(command);
+	this.in_stream = in_stream;
+	this.strip_quotes = strip_quotes;
+    }
+	
+    /** Returns the minumum number of remaining tokens before the tokenizer runs out of string. There may be more tokens than this count, but never less. The discrepancy is due to internal functionality and the fact we can't read ahead in the string or associated stream without risking the need for unpredictable push-back
+     * @return the minumum number of tokens available as an int
+     */
+    public int countTokens() {
+	if(count == 0 && internal_tokenizer.countTokens() > 1) {
+	    return 1;
+	}
+	if(count == -1) {
+	    count = internal_tokenizer.countTokens();
+	}
+	return count;
+    }
+    
+    /** Determine if there are still tokens available.
+     * @return true if there are more tokens, false otherwise
+     */
+    public boolean hasMoreTokens() {
+	return internal_tokenizer.hasMoreTokens();
+    }
+    
+    /** Method to retrieve the next token from the command, taking care to group tokens enclosed in speech marks.
+     * @return a String containing the next token from the command
+     */
+    public String nextToken() {
+	String result = null;
+	if(internal_tokenizer.hasMoreTokens()) {
+	    StringBuffer buffer = new StringBuffer(internal_tokenizer.nextToken());
+	    switch(buffer.charAt(0)) {
+	    case StaticStrings.DOUBLEQUOTE_CHAR:
+		///ystem.err.println("Building token wrapped by double quotes.");
+		result = buildToken(buffer, StaticStrings.DOUBLEQUOTE_CHAR, this.strip_quotes);
+		break;
+	    case StaticStrings.SINGLEQUOTE_CHAR:
+		///ystem.err.println("Building token wrapped by single quotes.");
+		result = buildToken(buffer, StaticStrings.SINGLEQUOTE_CHAR, this.strip_quotes);
+		break;
+	    case StaticStrings.OPENBRACKET_CHAR:
+		///ystem.err.println("Building token wrapped by brackets.");
+		result = buildToken(buffer, StaticStrings.CLOSEBRACKET_CHAR, false);
+		break;
+	    default:
+		///ystem.err.println("Returning plain string.");
+		result = buffer.toString();
+	    }
+	    buffer = null;
+	}
+	// Because of our tricky counting system we never want to have negative tokens remaining. In fact, unless the internal string buffer is empty, we will return a count of 1 anyway
+	if(count > 0) {
+	    count = count - 1;
+	}
+	///ystem.err.println("----- CommandTokenizer -----\ntoken:\t" + result + "\n----------------------------");
+	return result;
+    }
+    
+    /** Parse in the next token. paying heed to enclosing characters demands, escaped characters, newlines and empty buffers and consequential unexpected end of tokens
+     * @param buffer the StringBuffer in which the partial token is stored (at the first bit that caused this method to be called)
+     * @param end_char the sentinel char we are watching for as it encloses a token
+     * @param strip_characters a boolean denoting whether the enclosing characters should be stripped off
+     * @return the token, either in its entirity less the enclosing characters if required or, if an unexpected end occured, whatever we parsed without its starting enclosing character, again only if required. In fact if we weren't asked to strip characters then we add the enclosing character back in
+     */
+    private String buildToken(StringBuffer buffer, char end_char, boolean strip_characters) {
+	while(buffer.length() == 1 || buffer.charAt(buffer.length() - 1) != end_char || (buffer.length() > 3 && buffer.charAt(buffer.length() - 2) == StaticStrings.BACKSLASH_CHAR)) {
+	    try {
+		// The first version is for the basic tokenizer which has no idea of an input stream, so runs out tokens at the same time as the internal tokenizer does
+		if(internal_tokenizer.hasMoreTokens()) {
+		    buffer.append(StaticStrings.SPACE_CHAR);
+		    buffer.append(internal_tokenizer.nextToken());
+		}
+		// While the second version can draw more lines from the stream until eof occurs
+		else if(in_stream != null) {
+		    String line_str = null;
+		    while(!internal_tokenizer.hasMoreTokens() && (line_str = in_stream.readLine()) != null) {
+			///atherer.println("+++++ CommandTokenizer +++++\nappend:\t" + line_str + "\n+++++++++++++++++++++++++++++");
+			// Its at this stage the our token count becomes completely putu
+			internal_tokenizer = new StringTokenizer(line_str);
+			buffer.append(StaticStrings.NEW_LINE_CHAR); // A new line in the final token
+		    }
+		    line_str = null;
+		    if(internal_tokenizer.hasMoreTokens()) {
+			// Don't add a space if we just added a newline
+			if(buffer.charAt(buffer.length() - 1) != StaticStrings.NEW_LINE_CHAR) {
+			    buffer.append(StaticStrings.SPACE_CHAR);
+			}
+			buffer.append(internal_tokenizer.nextToken());
+		    }
+		    // We've prematurely run out of content, so throw the dummy, or at least return whatever we managed to parse sans its opening character
+		    else {
+			if(strip_characters) {
+			    return buffer.substring(1);
+			}
+			else {
+			    buffer.append(end_char);
+			    return buffer.toString();
+			}
+		    }
+		}
+		// We've prematurely run out of content, so throw the dummy, or at least return whatever we managed to parse sans its opening character
+		else {
+		    if(strip_characters) {
+			return buffer.substring(1);
+		    }
+		    else {
+			buffer.append(end_char);
+			return buffer.toString();
+		    }
+		}
+	    }
+	    // Exception thrown when we attempted reading from the input stream, so throw the dummy, or at least return whatever we managed to parse sans its opening character
+	    catch(Exception exception) {
+		DebugStream.printStackTrace(exception);
+		if(strip_characters) {
+		    return buffer.substring(1);
+		}
+		else {
+		    buffer.append(end_char);
+		    return buffer.toString();
+		}
+	    }
+	}
+	// Return the string sans enclosing characters
+	if(buffer.length() >= 2 && strip_characters) {
+	    return buffer.substring(1, buffer.length() - 1);
+	}
+	else {
+	    return buffer.toString();
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Control.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Control.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Control.java	(revision 31635)
@@ -0,0 +1,36 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+/**
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3d
+ */
+public interface Control {
+    public void destroy();
+    public void gainFocus();
+    public void loseFocus();
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DOMProxyListEntry.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DOMProxyListEntry.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DOMProxyListEntry.java	(revision 31635)
@@ -0,0 +1,38 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import org.w3c.dom.*;
+
+public interface DOMProxyListEntry 
+    extends Comparable {
+    public DOMProxyListEntry create(Element element);
+    public Element getElement();
+    public boolean isAssigned();
+    public void setAssigned(boolean assigned);
+    public void setElement(Element element);
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DOMProxyListModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DOMProxyListModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DOMProxyListModel.java	(revision 31635)
@@ -0,0 +1,300 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.util.*;
+import javax.swing.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+/** This class provides ListModel like access to a list of nodes within a DOM model.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3d
+ */
+public class DOMProxyListModel
+extends AbstractListModel {
+    protected Element root;
+    private DOMProxyListEntry class_type;
+    private HashMap cache = new HashMap ();
+    private NodeList children = null;
+    private String tag_name;
+    
+    /** Constructor.
+     * @param root the Element at the root of the subtree to be searched for appropriate child elements
+     * @param tag_name the name of appropriate elements as a String
+     * @param class_type the type of object to wrap the elements returned in, as a DOMProxyListEntry
+     */
+    public DOMProxyListModel (Element root, String tag_name, DOMProxyListEntry class_type) {
+        this.class_type = class_type;
+        this.root = root;
+        this.tag_name = tag_name;
+        this.children = this.root.getElementsByTagName (this.tag_name);
+    }
+    
+    /** Used to add an element into the underlying dom, and fire the appropriate repaint events. This version always adds the new element at the very head of the DOM. */
+    public synchronized void add (DOMProxyListEntry entry) {
+        Element element = entry.getElement ();
+        if(root.hasChildNodes ()) {
+            Node sibling = root.getFirstChild ();
+            root.insertBefore (element, sibling);
+            sibling = null;
+        }
+        else {
+            root.appendChild (element);
+        }
+        element = null;
+        // Regardless fire update event
+        cache.clear ();
+        fireIntervalAdded (this, 0, 0);
+    }
+    
+    /** Used to add an element into the underlying dom, and fire the appropriate repaint events.
+     * @param index the index where the element should be inserted (relative of the other elements in this proxy list)
+     * @param entry the <strong>DOMProxyListEntry</strong> to be inserted
+     */
+    public synchronized void add (int index, DOMProxyListEntry entry) {
+        ///atherer.println("Add entry at " + index + " where size = " + getSize());
+        Element element = entry.getElement ();
+        // retrieve the node where we want to insert
+        if(index < children.getLength ()) {
+            Node sibling = children.item (index);
+            // Find the parent node
+            Node parent_node = sibling.getParentNode ();
+            parent_node.insertBefore (element, sibling);
+            sibling = null;
+        }
+        // If the index is too large, we are adding to the end of our list of entries. However you have to remember that this list is only a viewport on the entire DOM so there might be entries following this group that we actually want to insert before (not append at the very end!)
+        else {
+            // Retrieve the currently last entry
+            index = children.getLength () - 1;
+            Node sibling = null;
+            Node parent_node = null;
+            if(index >= 0) {
+                sibling = children.item (index);
+                parent_node = sibling.getParentNode ();
+                sibling = sibling.getNextSibling ();
+            }
+            if(sibling != null && parent_node != null) {
+                parent_node.insertBefore (element, sibling);
+            }
+            // Add to the root node
+            else {
+                index = 0;
+                root.appendChild (element);
+            }
+        }
+        // Regardless fire update event
+        cache.clear ();
+        fireIntervalAdded (this, index, index);
+    }
+    
+    /** Used to add an element into the underlying dom, and fire the appropriate repaint events. This version inserts the new entry immediately -after- the given entry in the DOM.
+     * @param entry the DOMProxyListEntry to be inserted
+     * @param preceeding_entry the DOMProxyListEntry immediately before where we want the new entry
+     */
+    public synchronized void addAfter (DOMProxyListEntry entry, DOMProxyListEntry preceeding_entry) {
+        Element element = entry.getElement ();
+        Element preceeding_sibling = preceeding_entry.getElement ();
+        Node parent_node = preceeding_sibling.getParentNode ();
+        Node following_sibling = preceeding_sibling.getNextSibling ();
+        if(following_sibling != null) {
+            parent_node.insertBefore (element, following_sibling);
+        }
+        else {
+            parent_node.appendChild (element);
+        }
+        // Regardless fire update event
+        cache.clear ();
+        int index = indexOf (entry);
+        fireIntervalAdded (this, index, index);
+    }
+    
+    /** Used to add an element into the underlying dom, and fire the appropriate repaint events. This version inserts the new entry immediately -before- the given entry in the DOM.
+     * @param entry the DOMProxyListEntry to be inserted
+     * @param following_entry the DOMProxyListEntry immediately after where we want the new entry
+     */
+    public synchronized void addBefore (DOMProxyListEntry entry, DOMProxyListEntry following_entry) {
+        Element element = entry.getElement ();
+        Element following_sibling = following_entry.getElement ();
+        Node parent_node = following_sibling.getParentNode ();
+        parent_node.insertBefore (element, following_sibling);
+        // Regardless fire update event
+        cache.clear ();
+        int index = indexOf (entry);
+        fireIntervalAdded (this, index, index);
+    }
+    
+    public synchronized void add (Node parent, DOMProxyListEntry entry, Node sibling) {
+        Element child = entry.getElement ();
+        if(sibling != null) {
+            parent.insertBefore (child, sibling);
+        }
+        else {
+            parent.appendChild (child);
+        }
+        cache.clear ();
+        int index = indexOf (entry);
+        
+        
+        fireIntervalAdded (this, index, index);
+    }
+    
+    /** Used to add an element into the underlying dom, and fire the appropriate repaint events. This version always adds the new element at the end of the children. */
+    public synchronized void append (DOMProxyListEntry entry) {
+        Element element = entry.getElement ();
+        root.appendChild (element);
+        element = null;
+        // Regardless fire update event
+        cache.clear ();
+        fireIntervalAdded (this, 0, 0);
+    }
+    
+    public synchronized ArrayList children () {
+        ArrayList child_list = new ArrayList ();
+        int child_count = children.getLength ();
+        for(int i = 0; i < child_count; i++) {
+            child_list.add (getElementAt (i));
+        }
+        return child_list;
+    }
+    
+    public synchronized boolean contains (Object entry) {
+        boolean found = false;
+        int size = getSize ();
+        
+        for(int i = 0; !found && i < size; i++) {
+            DOMProxyListEntry sibling = (DOMProxyListEntry) getElementAt (i);
+            if(sibling.equals (entry)) {
+                
+                found = true;
+            }
+        }
+        return found;
+    }
+    
+    public synchronized Object getElementAt (int index) {
+        /** There are times when the length of the 'cache' is not as same as the length of the 'children', etc. not up to date. 
+            eg, when a classifier has been deleted. So we rather not have this efficiency for Format4gs3.java.*/       
+        if (class_type instanceof Format4gs3) {
+            Element element = (Element) children.item (index);
+            // Now wrap it in the object of the users choice
+            Object object = class_type.create (element);
+            return object;
+        }
+        
+        Object object = cache.get (new Integer (index));
+        if (object != null) {
+            return object;
+        }
+        
+        // Retrieve the required element
+        Element element = (Element) children.item (index);
+	if (element == null) {
+	  return null;
+	}
+        DebugStream.println ("Element at index " + index + " not in cache: " + element);
+        
+        // Now wrap it in the object of the users choice
+        object = class_type.create (element);
+        cache.put (new Integer (index), object);
+        return object;
+    }
+    
+    public synchronized int indexOf (DOMProxyListEntry entry) {
+        Element element = entry.getElement ();
+        int children_length = children.getLength ();
+        for(int i = 0; i < children_length; i++) {
+            Node node = children.item (i);
+            if(element == node) {
+                return i;
+            }
+        }
+        return -1;
+    }
+    
+    public synchronized int getSize () {
+        
+        if(children == null) {
+            children = root.getElementsByTagName (tag_name);
+        }
+        return children.getLength ();
+    }
+    
+    public synchronized void refresh () {
+        fireContentsChanged (this, 0, getSize ());
+    }
+    
+    public synchronized void refresh (DOMProxyListEntry entry) {
+        int index = indexOf (entry);
+        fireContentsChanged (this, index, index);
+    }
+    
+    public synchronized void remove (DOMProxyListEntry entry) {
+        remove (indexOf (entry));
+    }
+    
+    public synchronized void remove (int index) {
+        // Retrieve the required element
+        Node node = children.item (index);
+        // Find its parent
+        Node parent_node = node.getParentNode ();
+        // Now remove it
+        parent_node.removeChild (node);
+        // Refresh model
+        cache.clear ();
+            
+        fireIntervalRemoved (this, index, index);
+    }
+    
+    
+    /** Changes the 'root' element that this list sources its information from.
+     * @param  root the new root Element
+     */
+    public synchronized void setRoot (Element root) {
+        this.children = null;
+        cache.clear ();
+        this.root = root;
+        this.children = this.root.getElementsByTagName (this.tag_name);
+        fireContentsChanged (this, 0, getSize ());
+    }
+    
+    public synchronized void setAssigned (boolean assigned) {
+        if (assigned) {
+            root.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+        }
+        else {
+            root.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+        }
+    }
+    
+    public boolean isAssigned () {
+        return (root.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals ("") || root.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.TRUE_STR));
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DatabaseTypeManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DatabaseTypeManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DatabaseTypeManager.java	(revision 31635)
@@ -0,0 +1,356 @@
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.EventListener;
+import javax.swing.event.EventListenerList;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.ModalDialog;
+
+public class DatabaseTypeManager
+{
+
+	/** The size of this new collection dialog box. */
+	static private Dimension DIALOG_SIZE = new Dimension(600, 280);
+
+	static final public String DATABASE_TYPE_GDBM = "gdbm";
+	static final public String DATABASE_TYPE_JDBM = "jdbm";
+	static final public String DATABASE_TYPE_SQLITE = "sqlite";
+
+	static final public String DATABASE_TYPE_GDBM_STR = "GDBM";
+	static final public String DATABASE_TYPE_JDBM_STR = "JDBM";
+	static final public String DATABASE_TYPE_SQLITE_STR = "SQLITE";
+
+	static final public String[] DATABASE_TYPES = { DATABASE_TYPE_GDBM, DATABASE_TYPE_JDBM, DATABASE_TYPE_SQLITE };
+
+	private EventListenerList listeners = null;
+	/** the databasetype element in the config file - uses CollectionMeta */
+	public CollectionMeta database_type_meta = null;
+	private Control controls = null;
+
+	protected DatabaseTypeManager manager = null;
+
+	public DatabaseTypeManager()
+	{
+		database_type_meta = new CollectionMeta(CollectionDesignManager.collect_config.getDatabaseType());
+		if (getDatabaseType().equals(""))
+		{
+			database_type_meta.setValue(DATABASE_TYPE_GDBM);
+			// must have an old collection, assume MG
+		}
+		listeners = new EventListenerList();
+		manager = this;
+	}
+
+	public void addDatabaseTypeListener(DatabaseTypeListener listener)
+	{
+		listeners.add(DatabaseTypeListener.class, listener);
+	}
+
+	protected void notifyListeners(String new_database_type)
+	{
+		Object[] concerned = listeners.getListenerList();
+		for (int i = 0; i < concerned.length; i++)
+		{
+			if (concerned[i] == DatabaseTypeListener.class)
+			{
+				((DatabaseTypeListener) concerned[i + 1]).databaseTypeChanged(new_database_type);
+			}
+		}
+		concerned = null;
+	}
+
+	public void promptForNewDatabaseType()
+	{
+
+		DatabaseTypePrompt dtp = new DatabaseTypePrompt(database_type_meta.getValue(CollectionMeta.TEXT));
+	}
+
+	public boolean isGDBM()
+	{
+
+		return getDatabaseType().equals(DATABASE_TYPE_GDBM);
+	}
+
+	public boolean isJDBM()
+	{
+
+		return getDatabaseType().equals(DATABASE_TYPE_JDBM);
+	}
+
+	public boolean isSQLITE()
+	{
+
+		return getDatabaseType().equals(DATABASE_TYPE_SQLITE);
+	}
+
+	public String getDatabaseType()
+	{
+		return database_type_meta.getValue(CollectionMeta.TEXT);
+	}
+
+	public Control getControls()
+	{
+		if (controls == null)
+		{
+			controls = new DatabaseTypeControl();
+		}
+		return controls;
+	}
+
+	public interface DatabaseTypeListener extends EventListener
+	{
+		public void databaseTypeChanged(String new_database_type);
+	}
+
+	private class DatabaseTypeControl extends JPanel implements Control, DatabaseTypeListener
+	{
+
+		JLabel label = null;
+		JButton change_button = null;
+
+		public DatabaseTypeControl()
+		{
+			super();
+			this.setComponentOrientation(Dictionary.getOrientation());
+
+			JPanel spacer_panel = new JPanel();
+			spacer_panel.setComponentOrientation(Dictionary.getOrientation());
+
+			JPanel main_panel = new JPanel();
+			main_panel.setComponentOrientation(Dictionary.getOrientation());
+			/*
+			 * may be CDM.DatabaseTypeManager.gdbm,
+			 * CDM.DatabaseTypeManager.jdbm, CDM.DatabaseTypeManager.sqlite
+			 */
+			label = new JLabel(Dictionary.get("CDM.DatabaseTypeManager.Current_Type", getDatabaseTypeString(getDatabaseType())));
+			label.setComponentOrientation(Dictionary.getOrientation());
+
+			change_button = new GLIButton(Dictionary.get("CDM.DatabaseTypeManager.Change"), Dictionary.get("CDM.DatabaseTypeManager.Change_Tooltip"));
+
+			change_button.addActionListener(new ActionListener()
+			{
+				public void actionPerformed(ActionEvent event)
+				{
+					promptForNewDatabaseType();
+				}
+			});
+
+			main_panel.setLayout(new BorderLayout(10, 10));
+			main_panel.add(label, BorderLayout.CENTER);
+			main_panel.add(change_button, BorderLayout.LINE_END);
+
+			setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));
+			setLayout(new BorderLayout());
+			add(spacer_panel, BorderLayout.CENTER);
+			add(main_panel, BorderLayout.LINE_END);
+
+			manager.addDatabaseTypeListener(this);
+		}
+
+		public void loseFocus()
+		{
+		}
+
+		public void gainFocus()
+		{
+		}
+
+		public void destroy()
+		{
+		}
+
+		private String getDatabaseTypeString(String database_type)
+		{
+			if (database_type.equals(DATABASE_TYPE_GDBM))
+			{
+				return DATABASE_TYPE_GDBM_STR;
+			}
+			if (database_type.equals(DATABASE_TYPE_JDBM))
+			{
+				return DATABASE_TYPE_JDBM_STR;
+			}
+			if (database_type.equals(DATABASE_TYPE_SQLITE))
+			{
+				return DATABASE_TYPE_SQLITE_STR;
+			}
+			return "";
+		}
+
+		public void databaseTypeChanged(String new_database_type)
+		{
+			label.setText(Dictionary.get("CDM.DatabaseTypeManager.Current_Type", getDatabaseTypeString(new_database_type)));
+		}
+	}
+
+	private class DatabaseTypePrompt extends ModalDialog
+	{
+
+		private JDialog self;
+
+		private JRadioButton gdbm_button = null;
+		private JRadioButton jdbm_button = null;
+		private JRadioButton sqlite_button = null;
+
+		private JTextArea description_textarea = null;
+
+		JButton ok_button = null;
+		JButton cancel_button = null;
+
+		public DatabaseTypePrompt(String current_database_type)
+		{
+			super(Gatherer.g_man, true);
+			this.self = this;
+			setSize(DIALOG_SIZE);
+			setTitle(Dictionary.get("CDM.DatabaseTypeManager.Title"));
+			this.setComponentOrientation(Dictionary.getOrientation());
+			gdbm_button = new JRadioButton(DATABASE_TYPE_GDBM_STR);
+			gdbm_button.setComponentOrientation(Dictionary.getOrientation());
+			gdbm_button.setActionCommand(DATABASE_TYPE_GDBM);
+			jdbm_button = new JRadioButton(DATABASE_TYPE_JDBM_STR);
+			jdbm_button.setComponentOrientation(Dictionary.getOrientation());
+			jdbm_button.setActionCommand(DATABASE_TYPE_JDBM);
+			sqlite_button = new JRadioButton(DATABASE_TYPE_SQLITE_STR);
+			sqlite_button.setComponentOrientation(Dictionary.getOrientation());
+			sqlite_button.setActionCommand(DATABASE_TYPE_SQLITE);
+
+			DatabaseTypeButtonListener dtbl = new DatabaseTypeButtonListener();
+			gdbm_button.addActionListener(dtbl);
+			jdbm_button.addActionListener(dtbl);
+			sqlite_button.addActionListener(dtbl);
+
+			ButtonGroup database_type_group = new ButtonGroup();
+			database_type_group.add(gdbm_button);
+			database_type_group.add(jdbm_button);
+			if (!Gatherer.GS3)
+			{
+				database_type_group.add(sqlite_button);
+			}
+			if (current_database_type != null)
+			{
+				if (current_database_type.equals(DATABASE_TYPE_GDBM))
+				{
+					gdbm_button.setSelected(true);
+				}
+				else if (current_database_type.equals(DATABASE_TYPE_JDBM))
+				{
+					jdbm_button.setSelected(true);
+				}
+				else if (current_database_type.equals(DATABASE_TYPE_SQLITE) && !Gatherer.GS3)
+				{
+					sqlite_button.setSelected(true);
+				}
+			}
+
+			JPanel radio_pane = new JPanel();
+			radio_pane.setLayout(new GridLayout(3, 1));
+			radio_pane.add(gdbm_button);
+			radio_pane.add(jdbm_button);
+			if (!Gatherer.GS3)
+			{
+				radio_pane.add(sqlite_button);
+			}
+			radio_pane.setComponentOrientation(Dictionary.getOrientation());
+
+			description_textarea = new JTextArea();
+			description_textarea.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+			/*
+			 * may be CDM.DatabaseTypeManager.gdbm_Description,
+			 * CDM.DatabaseTypeManager.jdbm_Description,
+			 * CDM.DatabaseTypeManager.sqlite_Description
+			 */
+			description_textarea.setText(Dictionary.get("CDM.DatabaseTypeManager." + current_database_type + "_Description"));
+			description_textarea.setLineWrap(true);
+			description_textarea.setWrapStyleWord(true);
+			description_textarea.setComponentOrientation(Dictionary.getOrientation());
+
+			cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+
+			cancel_button.addActionListener(new ActionListener()
+			{
+				public void actionPerformed(ActionEvent event)
+				{
+					self.dispose();
+				}
+			});
+
+			ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+
+			ok_button.addActionListener(new ActionListener()
+			{
+				public void actionPerformed(ActionEvent event)
+				{
+					String new_database_type = DATABASE_TYPE_GDBM;
+					if (gdbm_button.isSelected())
+					{
+						new_database_type = DATABASE_TYPE_GDBM;
+					}
+					else if (jdbm_button.isSelected())
+					{
+						new_database_type = DATABASE_TYPE_JDBM;
+					}
+					else if (sqlite_button.isSelected())
+					{
+						new_database_type = DATABASE_TYPE_SQLITE;
+					}
+					if (!database_type_meta.getValue(CollectionMeta.TEXT).equals(new_database_type))
+					{
+						manager.notifyListeners(new_database_type);
+						database_type_meta.setValue(new_database_type);
+					}
+					self.dispose();
+				}
+			});
+			// tell the CDM that we have (possibly) changed
+			ok_button.addActionListener(CollectionDesignManager.databasecol_change_listener);
+			JPanel button_pane = new JPanel();
+			button_pane.setLayout(new GridLayout(1, 2));
+			button_pane.add(ok_button);
+			button_pane.add(cancel_button);
+			button_pane.setComponentOrientation(Dictionary.getOrientation());
+
+			JPanel content_pane = (JPanel) getContentPane();
+			content_pane.setOpaque(true);
+			content_pane.setLayout(new BorderLayout());
+			content_pane.add(radio_pane, BorderLayout.NORTH);
+			content_pane.add(new JScrollPane(description_textarea), BorderLayout.CENTER);
+			content_pane.add(button_pane, BorderLayout.SOUTH);
+			content_pane.setComponentOrientation(Dictionary.getOrientation());
+
+			// Center and display.
+			Dimension screen_size = Configuration.screen_size;
+			this.setLocation((screen_size.width - DIALOG_SIZE.width) / 2, (screen_size.height - DIALOG_SIZE.height) / 2);
+			this.setVisible(true); // blocks until the dialog is killed
+
+		}
+
+		public void loseFocus()
+		{
+
+		}
+
+		public void gainFocus()
+		{
+
+		}
+
+		public void destroy()
+		{
+
+		}
+
+		private class DatabaseTypeButtonListener implements ActionListener
+		{
+
+			public void actionPerformed(ActionEvent event)
+			{
+				description_textarea.setText(Dictionary.get("CDM.DatabaseTypeManager." + event.getActionCommand() + "_Description"));
+			}
+		}
+	}
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DepositorMetadataManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DepositorMetadataManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DepositorMetadataManager.java	(revision 31635)
@@ -0,0 +1,526 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.util.ArrayList;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.GComboBox;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSet;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.util.Utility;
+
+/**
+ * This manager implements the "Depositor Metadata" section in GLI Format Panel, where 
+ * users can customize which metadata elements will be used to describe an item when deposited in the Depositor.
+ *      
+ * @author Anna Huang, Greenstone Digital Library, University of Waikato
+ * @version 1.0
+ */
+public class DepositorMetadataManager {
+    /** Display controls of the element */
+    static private Dimension LABEL_SIZE = new Dimension(150, 15);
+    static private Dimension ROW_SIZE = new Dimension(400, 20);
+    static private Dimension MAX_ROW_SIZE = new Dimension(1000, 30);
+    
+    /** Available types, corresponds to the HTML input types */ 
+    static private String[] TYPEOPTIONS = new String[]{"text", "textarea"};
+    /** Default elements to be used in Depositor */
+    static private String DEFAULT_METADATA_ELEMENTS = "dc.Title, dc.Creator, dc.Description";
+    /** Definition attribute of a metadata element */ 
+    static private String DEFINITION = "definition";
+    
+    /** Display controls */
+    private Control controls;	
+    /** List of MetadataElementEntry objects, each represents a metadata element */
+    private ArrayList metadata_checklist_model = null;	
+    /** Collection configuration file manager */ 
+    private CollectionMetaManager collmeta_manager = CollectionDesignManager.collectionmeta_manager;
+    
+    public DepositorMetadataManager() {
+    }
+    
+    /** Destructor. */
+    public void destroy() {
+	if (controls != null) {
+	    controls.destroy();
+	    controls = null;
+	}
+	if(metadata_checklist_model != null) {
+	    metadata_checklist_model.clear();
+	    metadata_checklist_model = null;
+	}
+    }
+		
+    public Control getControls() {
+	if (controls == null) {
+	    controls = new DepositorControl();
+	}
+	return controls;
+    }
+	
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+    }
+	
+    /**
+     * The implementation of this display control is similar to the <code>ArgumentConfiguration</code>, 
+     * where each component in the central panel is another JPanel component consists of a checkbox and a drop down list. 
+     */
+    private class DepositorControl
+	extends JPanel
+	implements Control {		
+		
+	private JPanel central_pane = null;
+
+	public DepositorControl() {
+	    super();
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    JPanel header_panel = new DesignPaneHeader("CDM.GUI.DepositorMetadata", "depositormetadatasettings");
+			
+	    // load all possible metadata sets, and existing configuration
+	    buildModel();
+	    
+	    central_pane = new JPanel();
+            central_pane.setComponentOrientation(Dictionary.getOrientation());
+	    central_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    central_pane.setLayout(new BoxLayout(central_pane, BoxLayout.Y_AXIS));	
+									
+	    // build the central pane, ie. create gui component for each metadata element 
+	    buildPane();		
+
+	    JPanel collection_checklist_pane = new JPanel();
+            collection_checklist_pane.setComponentOrientation(Dictionary.getOrientation());
+	    collection_checklist_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    collection_checklist_pane.setLayout(new BorderLayout());
+	    collection_checklist_pane.add(new JScrollPane(central_pane), BorderLayout.CENTER);
+
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(header_panel, BorderLayout.NORTH);
+	    add(collection_checklist_pane, BorderLayout.CENTER);
+	}
+		
+	public void destroy() {
+	}
+
+	public void gainFocus() {
+	}
+		
+	public void loseFocus() {			
+	    StringBuffer converted_javascript = new StringBuffer();
+			
+	    // get currently selected metadata elements and create the javascript associative array variable
+	    if (metadata_checklist_model != null) {
+		int size = metadata_checklist_model.size();
+		for(int i = 0; i < size; i++) {				
+		    MetadataElementEntry entry = (MetadataElementEntry) metadata_checklist_model.get(i);
+					
+		    if (entry.isSelected()) {					
+			StringBuffer element = new StringBuffer();
+			element.append("{\"name\":\"").append(entry.name).append("\",");
+			element.append("\"label\":\"").append(entry.label).append("\",");
+			element.append("\"tooltip\":\"").append(entry.name).append(": ").append(entry.tooltip).append("\",");
+			element.append("\"type\":\"").append(entry.getType()).append("\"}");
+			if(converted_javascript.length() != 0) {
+			    converted_javascript.append(", ");
+			}
+			converted_javascript.append(element);
+			element = null;
+		    }				
+		}
+				
+		// if none of the elements were selected, use the default setting				
+		if (converted_javascript.length() == 0) {
+		    System.err.println("Error: DepositorMetadataMananger should have at least one selected metadata element.");
+		}
+
+		// save to the configuration file
+		CollectionMeta depositor_metadata_meta = collmeta_manager.getMetadatum("depositormetadata", true);
+		depositor_metadata_meta.setValue(converted_javascript.toString());			
+	    }		
+	}
+		
+	private void buildModel()
+	{
+	    metadata_checklist_model = new ArrayList();
+	    String current_coll_name = CollectionManager.getLoadedCollectionName();
+			
+	    // get the elements that have already been used in depositor
+	    // add the "depositormetadata" meta if not found in the configuration file
+	    CollectionMeta depositor_metadata_meta = collmeta_manager.getMetadatum("depositormetadata", true);			
+	    String selected_metadata_javascript = depositor_metadata_meta.getValue(true);
+			
+	    // load all MDSets in the collection, ie. .mds files in the collection's metadata directory
+	    String file_name = Gatherer.getCollectDirectoryPath() + File.separator + current_coll_name + File.separator + "metadata";
+	    ArrayList metadata_sets = MetadataSetManager.listMetadataSets(new File(file_name));
+	    if (metadata_sets != null) {
+	      // unload the ex mds
+	      for (int i = 0, j = metadata_sets.size(); i < j; i++) {
+		if (((MetadataSet) metadata_sets.get(i)).getNamespace().equals("ex")) {
+		  metadata_sets.remove(i);
+		  break;
+		}
+	      }
+	    }
+	    
+	    if (metadata_sets == null) {
+	      metadata_sets = new ArrayList();
+	    }
+	    if (metadata_sets.size() == 0) {
+	      // load dublin core
+		file_name = org.greenstone.gatherer.gems.MetadataSetManager.getGLIMetadataDirectoryPath();
+		file_name += "dublin.mds";
+		File dublin_mds_file = new File(file_name);
+		if (dublin_mds_file.exists()) {
+		  MetadataSetManager.loadMetadataSet(dublin_mds_file);	
+		  MetadataSet dc = MetadataSetManager.getMetadataSet("dc");
+		  metadata_sets.add(dc);
+		}
+	    }
+	    // if we are still empty :-(
+	    if (metadata_sets.size() == 0) {
+		System.err.println("Error: DepositorMetadataMananger can't find any valid metadata set files.");
+		return;
+	    }			
+			
+	    // load all the metadata elements
+	    for (int i = 0, j = metadata_sets.size(); i < j; i++) {
+		MetadataSet metadata_set = (MetadataSet) metadata_sets.get(i);				
+		ArrayList elements = metadata_set.getMetadataSetElements(); 
+		
+		for (int k = 0; elements != null && k < elements.size(); k++) {
+		    MetadataElement element = (MetadataElement) elements.get(k);
+		    // all names are language independent at the moment. 
+		    MetadataElementEntry entry = new MetadataElementEntry (element.getFullName(), element.getName(), false);
+		    entry.setToolTip(element.getAttribute(DEFINITION, "en"));
+		    metadata_checklist_model.add(entry);
+		}
+	    }	
+	    
+	    if (!selected_metadata_javascript.equals("")) {
+		for (int i = 0, j = metadata_checklist_model.size(); i < j; i++) {
+		    MetadataElementEntry entry = (MetadataElementEntry) metadata_checklist_model.get(i);
+		    
+		    String temp = "\"name\":\"" + entry.name + "\"";
+		    if (selected_metadata_javascript.indexOf(temp) != -1) {
+			entry.setSelected(true);
+			entry.type = getValueFromJSAssociatedArray(selected_metadata_javascript, entry.name, "type");						
+		    }
+		    temp = null;
+		}	
+	    } else {				
+		// use default elements				
+		for (int i = 0, j = metadata_checklist_model.size(); i < j; i++) {
+		    MetadataElementEntry entry = (MetadataElementEntry) metadata_checklist_model.get(i);
+		    if (DEFAULT_METADATA_ELEMENTS.indexOf(entry.name) != -1) {
+			entry.setSelected(true);
+			entry.type = TYPEOPTIONS[0];
+		    }
+		}
+	    }					
+	}	
+	
+	private void buildPane() {
+	    // use different background color for two adjacent metadata sets
+	    String current_namespace = null;
+	    String current_bgcolor_name = null;
+	    for (int i = 0, j = metadata_checklist_model.size(); i < j; i++) {
+		MetadataElementEntry entry = (MetadataElementEntry) metadata_checklist_model.get(i);
+		
+		if (current_namespace == null) {
+		    current_namespace = entry.namespace;
+		    current_bgcolor_name = "coloring.collection_tree_background";
+		} else {
+		    if (!entry.namespace.equals(current_namespace)) {
+			current_namespace = entry.namespace;
+			current_bgcolor_name = (current_bgcolor_name == "coloring.collection_tree_background") ? "coloring.collection_heading_background" : "coloring.collection_tree_background";
+		    }
+		}			
+		
+		if (entry.getGUIControl() == null) {
+		    MetadataElementControl element_control = entry.initGUIControl(current_bgcolor_name);					
+		    central_pane.add(element_control, BorderLayout.NORTH);
+		}				
+	    }
+	    
+	}
+		
+	/**
+	 * Retrieve the value for the specified element in the given javascript associative array expression
+	 *  
+	 * @param text
+	 * @param identifier
+	 * @param elem_name
+	 * @return
+	 */
+	private String getValueFromJSAssociatedArray(String text, String identifier, String elem_name) {
+	    int p = text.indexOf(elem_name, text.indexOf(identifier));
+	    if (p == -1) {
+		return null;
+	    }
+	    p = text.indexOf("\":\"", p);
+	    if (p == -1) {
+		return null;
+	    }
+	    p += 3;
+			
+	    return text.substring(p, text.indexOf("\"", p));			
+	}
+    }
+	
+    private class MetadataElementControl extends JPanel {
+	
+	private JCheckBox enabled = null;
+	private JComponent value_control = null;		
+		
+	public MetadataElementControl (MetadataElementEntry entry, String background_color_name) {
+            
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    String tip = "<html>" + entry.tooltip + "</html>";
+	    tip = Utility.formatHTMLWidth(tip, 80);
+	    
+	    setBackground(Configuration.getColor(background_color_name, false));
+	    setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
+	    setLayout(new BorderLayout());
+	    setPreferredSize(ROW_SIZE);
+	    setMaximumSize(MAX_ROW_SIZE);
+			
+	    // the checkbox component
+	    enabled = new JCheckBox(entry.name);
+	    enabled.setComponentOrientation(Dictionary.getOrientation());
+	    enabled.setSelected(entry.selected);
+	    enabled.setOpaque(false);
+	    enabled.setPreferredSize(LABEL_SIZE);
+	    enabled.setToolTipText(entry.tooltip);
+	    add(enabled, BorderLayout.LINE_START);
+			
+	    // the drop down list component
+	    ArrayList option_list = new ArrayList();
+	    for (int i = 0; i < TYPEOPTIONS.length; i++) {
+		option_list.add(TYPEOPTIONS[i]);
+	    }
+	    value_control = new GComboBox(option_list.toArray(), false, false);
+	    value_control.setComponentOrientation(Dictionary.getOrientation());
+	    selectValue((JComboBox)value_control, entry.type); 
+	    value_control.setOpaque(true);			
+	    if (entry.selected) { 
+		value_control.setBackground(Color.white);
+		value_control.setEnabled(true);
+	    } else {
+		value_control.setBackground(Color.lightGray);
+		value_control.setEnabled(false);
+		value_control.setVisible(false);
+	    }							
+	    add(value_control, BorderLayout.CENTER);
+	    
+	    enabled.addActionListener(new EnabledListener(value_control));
+	}
+				
+	private boolean selectValue(JComboBox combobox, String target) {
+	    if (target == null) {
+		combobox.setSelectedIndex(0);
+		return false;
+	    }
+			
+	    for (int i = 0; i < combobox.getItemCount(); i++) {				
+		if (combobox.getItemAt(i).toString().equals(target)) {
+		    combobox.setSelectedIndex(i);
+		    return true;
+		}
+	    }
+	    return false;
+	}
+		
+	private class EnabledListener
+	    implements ActionListener {			
+	    private JComponent target = null;
+	    
+	    public EnabledListener(JComponent target) {
+		this.target = target;
+	    }
+	    
+	    public void actionPerformed(ActionEvent event) {
+		JCheckBox source = (JCheckBox)event.getSource();
+		
+		// check at least one of the elements should be ticked
+		checkTickedElements(source);
+				
+		// update the status of the drop-down list component associated with the current checkbox 
+		if (this.target == null) {
+		    return;
+		}				
+				
+		if(source.isSelected()) {
+		    target.setBackground(Color.white);
+		    target.setEnabled(true);
+		    target.setVisible(true);
+		}
+		else {
+		    target.setBackground(Color.lightGray);
+		    target.setEnabled(false);
+		    target.setVisible(false);
+		}				
+	    }
+			
+	    private void checkTickedElements(JCheckBox source) {
+		if (source.isSelected()) {
+		    return;
+		}
+				
+		boolean b = false;
+		for (int i = 0, j = metadata_checklist_model.size(); i < j; i++) {
+		    if (((MetadataElementEntry) metadata_checklist_model.get(i)).gui_object.enabled.isSelected()) {
+			b = true;
+			break;
+		    }
+		}
+				
+		// there must be at least one element selected in the list 
+		if (b == false) {
+		    Object[] options = {Dictionary.get("General.OK")};
+		    JOptionPane.showOptionDialog(Gatherer.g_man, Dictionary.get("CDM.DepositorMetadataManager.Warning"), Dictionary.get("General.Warning"), JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
+		    
+		    source.setSelected(true);
+		}
+	    }
+	}
+	
+    }
+	
+    private class MetadataElementEntry implements Comparable {
+	/** namespace, used to distinguish different metadata sets and use different colors when displaying */
+	String namespace = null;
+	/** identifier, with namespace */
+	String name = null;
+	/** language independent label, without namespace, used for display in the depositor web page, eg. Title */
+	String label = null;	
+	/** type of the element: text or textarea */
+	String type = null;
+	/** description of the element, used in web page */
+	String tooltip = null;		
+	/** whether current element is selected or not */
+	boolean selected ;
+	/** GUI component object associated with this element */ 
+	MetadataElementControl gui_object = null;
+		
+	/**
+	 * Constructor
+	 * @param name Language independent identifier of the element with namespace, eg. dc.Title
+	 * @param label Language independent label of the element, without namespace, eg. Title 
+	 * @param selected Whether the current element has been selected as depositor metadata element 
+	 */
+	public MetadataElementEntry(String name, String label, boolean selected) {
+	    this.name = name;
+	    this.label = label;
+	    this.selected = selected;
+	    
+	    if (name.indexOf(".") != -1) {
+		namespace = name.substring(0, name.indexOf("."));
+	    }
+	}
+		
+	/**
+	 * Initiate the <code>MetadataElementControl<code> GUI component object associated with this element, with the specified background color  
+	 * @param background_color_name System configured color name for the background, eg. "coloring.collection_heading_background", or "coloring.collection_tree_background"
+	 * @return the initialized component object
+	 */
+	public MetadataElementControl initGUIControl (String background_color_name) {
+	    gui_object = new MetadataElementControl(this, background_color_name);
+	    return gui_object;
+	}
+	
+	/**
+	 * Get the associated GUI component
+	 * @return
+	 */
+	public MetadataElementControl getGUIControl() {
+	    return gui_object;
+	}
+		
+	/**
+	 * Set selected
+	 * @param b
+	 */
+	public void setSelected(boolean b) {
+	    this.selected = b;
+	}
+		
+	public boolean isSelected() {
+	    return gui_object.enabled.isSelected();
+	}
+		
+	/**
+	 * Get the selected type for the element
+	 * @return type
+	 */
+	public String getType() {
+	    return ((JComboBox) gui_object.value_control).getSelectedItem().toString();
+	}
+		
+	public void setToolTip (String tip) {
+	    if (tip == null) {
+		tooltip = "";
+		return;
+	    }
+	    this.tooltip = tip;
+	}
+
+	public int compareTo(Object obj) {
+	    return compareTo((MetadataElementEntry) obj);
+	}
+		
+	public int compareTo(MetadataElementEntry obj) {
+	    return this.name.compareTo(obj.name);
+	}
+	
+    }
+    
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DesignChangeListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DesignChangeListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DesignChangeListener.java	(revision 31635)
@@ -0,0 +1,81 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Matthew Whyte, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.cdm;
+
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.DebugStream;
+import javax.swing.event.*;
+import java.awt.event.*;
+
+/**
+ * This handles events generated by changing design options.
+ * The rebuildTypeRequired is updated if the rebuild type associated with this
+ * listener requires more 'work' than what is currently set in the {@link CollectionDesignManager}.
+ * @author Matthew Whyte
+ */
+public class DesignChangeListener implements ActionListener, DocumentListener {
+    
+    /** The rebuild type required associated with this listener */
+    private final int rebuildType;
+    
+    /** Constructor. Sets the build type required. */
+    public DesignChangeListener(int type) {
+	rebuildType = type;
+    }
+
+    public void maybeSetRebuildRequired() {
+	if (rebuildType > CollectionDesignManager.getRebuildTypeRequired()) {
+	    CollectionDesignManager.setRebuildTypeRequired(rebuildType);
+	}
+    }
+    
+    /** Gives notification that an event has happened */
+    public void actionPerformed(ActionEvent event) {
+	//DebugStream.println("ActionEvent in Design from: " + event.getSource());
+	maybeSetRebuildRequired();
+    }
+
+    /** Gives notification that an attribute or set of attributes changed. */
+    public void changedUpdate(DocumentEvent e) {
+	maybeSetRebuildRequired();
+	//DebugStream.println("changedUpdate in Design.");
+    }
+    
+    /** Gives notification that there was an insert into the document. */
+    public void insertUpdate(DocumentEvent e) {
+	maybeSetRebuildRequired();
+	//DebugStream.println("insertUpdate in Design."); //debug
+    }
+    
+    /** Gives notification that a portion of the document has been removed. */
+    public void removeUpdate(DocumentEvent e) {
+	maybeSetRebuildRequired();
+	//DebugStream.println("removeUpdate in Design."); //debug
+    }
+    
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DynamicListModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DynamicListModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/DynamicListModel.java	(revision 31635)
@@ -0,0 +1,119 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+/**************************************************************************************
+ * Title:        Gatherer
+ * Description:  The Gatherer: a tool for gathering and enriching a digital collection.
+ * Company:      The University of Waikato
+ * Written:      20/05/02
+ * Revised:      03/10/02 - Commented
+ **************************************************************************************/
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultListModel;
+
+/** This class extends the functionality of the DefaultListModel to include simple methods for refreshing the contents of this model if one of its elements changes.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class DynamicListModel
+    extends DefaultListModel
+    implements ComboBoxModel {
+
+    private boolean auto_order = false;
+
+    private Object object = null;
+
+    public void addElement(Object element) {
+	if(auto_order) {
+	    // Insert the object in its alphabetically correct place.
+	    int position = 0;
+	    boolean found = false;
+	    while(!found && position < size()) {
+		///ystem.err.print("Compare " + element.toString() + " to " + get(position).toString() + ": ");
+		int order = element.toString().compareTo(get(position).toString());
+		if(order < 0) {
+		    add(position, element);
+		    found = true;
+		    ///ystem.err.println("Greater than. Insert");
+		}
+		else if(order == 0) {
+		    found = true;
+		    ///ystem.err.println("Equal. End.");
+		}
+		else {
+		    position++;
+		    ///ystem.err.println("Less than. Carry on.");
+		}
+	    }
+	    if(!found) {
+		super.addElement(element);
+		///ystem.err.println("Out of elements. Insert");
+	    }
+	}
+	else {
+	    super.addElement(element);
+	}
+    }
+
+    /* private DynamicListModel shallowCopy() {
+	DynamicListModel copy = new DynamicListModel();
+	copy.setAutoOrder(auto_order);
+	for(int i = 0; i < size(); i++) {
+	    copy.addElement(get(i));
+	}
+	return copy;
+	} */
+
+    public Object getSelectedItem() {
+	///ystem.err.println("Get item: " + object);
+	return object;
+    }
+    /** Notify all controls that are based on this list model that its contents have changed, and they should repaint themselves. */
+    public void refresh() {
+	fireContentsChanged(this, 0, size());
+    }
+
+    public void setAutoOrder(boolean auto_order) {
+	this.auto_order = auto_order;
+    }
+
+    public void setSelectedItem(Object object) {
+	///ystem.err.println("Set item: " + object);
+	this.object = object;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Format.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Format.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Format.java	(revision 31635)
@@ -0,0 +1,381 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+
+/** This class encapsulates all the information about a format command from the collection configuration. This includes both the 'general' formatting commands, and the commands targetted at specific classifiers or classification component types. This class is, unfortunately, the most complex in terms of DOM model support, since we have to maintain a live reference to any classifier this format applies to. We temporarily use the feature name as the format name attribute but since the order of classifiers can change, this of course gets horribly out of date. Before the CollectionDesignManager saves the CollectionConfiguration it calls a method in the FormatManager to force all of the Formats to update their name attributes.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class Format 
+    implements DOMProxyListEntry {
+
+    /** The default features (not all of these are in the Greenstone Developer's Guide). */
+    static final public String DEFAULT_FEATURES[] = { "", "AllowExtendedOptions", "Document", "DocumentArrowsBottom", "DocumentArrowsTop","DocumentSearchResultLinks", "DocumentButtons", "DocumentContents", "DocumentHeading", "DocumentImages", "DocumentText", "DocumentTitles", "DocumentUseHTML", "NavigationBar", "RelatedDocuments", "Search", "SearchTypes", "AllowUserComments" };
+    /** The list of known feature parts. */
+    static final public String DEFAULT_PARTS[] = { "", "DateList", "HList", "VList" };
+
+      
+    /** Only "" and "Search" can have parts */
+    static public boolean canHavePart(String name) {
+	return (name.startsWith("<html>") || name.equalsIgnoreCase("") || name.equalsIgnoreCase("Search") || name.equalsIgnoreCase("Document"));
+    }
+	
+    static public String generateName(Object feature, String part) {
+	if(feature instanceof Classifier) {
+	    return ((Classifier)feature).getPositionString() + part;
+	}
+	else {
+	    return feature.toString() + part;
+	}
+    }
+
+
+    /** Returns true if the parameter is a boolean type, false otherwise. */
+    static public boolean isParamType(String name)
+    {
+	if (name.equalsIgnoreCase("AllowExtendedOptions") || name.equalsIgnoreCase("DocumentArrowsBottom") || name.equalsIgnoreCase("DocumentArrowsTop") || name.equalsIgnoreCase("DocumentSearchResultLinks") || name.equalsIgnoreCase("DocumentContents") || name.equalsIgnoreCase("DocumentImages") || name.equalsIgnoreCase("DocumentTitles") || name.equalsIgnoreCase("DocumentUseHTML") || name.equalsIgnoreCase("AllowUserComments") ) {
+	    return true;
+	}
+	else {
+	    return false;
+	}
+    }
+
+   
+    /** If this format is altering a Classifier then this is the classifier in question. */
+    private Classifier classifier = null;
+    /** The element this format is based on. */
+    private Element element = null;
+    /** We keep a copy of the part because its slightly more computationally tricky to calculate. */
+    private String part = null;
+    /** We keep a copy of the feature name */
+    private String feature_name = null;
+    /** Cached result of toString. */
+    private String text = null;
+
+    /** Constructor only to be used during DOMProxyListModel initialization. */
+    public Format() {
+	   
+    }
+
+    public Format(Element element) {
+
+	this.element = element;
+	// Immediately check if we are dealing with a classifier, by retrieving the feature name. If it is a classifier, then restore the live reference immediately before anything changes it.
+	Object object = getFeature();
+
+	// There may be format statements for no-longer-existing classifiers
+	if (object == null) {
+	    DebugStream.println("No matching classifier for format statement!");
+	    return;
+	}
+
+	if(object instanceof Classifier) {
+	    classifier = (Classifier) object;
+	}
+	else {
+	    String feature = (String) object;
+	    if(feature.toUpperCase().startsWith(Classifier.CLASSIFIER_PREFIX)) {
+		// Extract the integer index
+		int index = Integer.parseInt(feature.substring(Classifier.CLASSIFIER_PREFIX.length()));
+		// Subtract one (as java is 0-2 where G2 is 1-3)
+		index = index - 1;
+		// Retrieve the appropriate classifier
+		classifier = CollectionDesignManager.classifier_manager.getClassifier(index);
+	    }
+	}
+
+      	 
+    }
+
+    /** Constructor for a flag format command.
+     * @param feature The <Strong>Object</strong> this format affects.
+     * @param part The specific part of the control to format as a <strong>String</strong>.
+     * @param state A <i>boolean</i> indicating this formats state.
+     */
+    public Format(Object feature, String part, boolean state) {
+
+	element = CollectionConfiguration.createElement(StaticStrings.FORMAT_ELEMENT);
+	setName(feature, part);
+	setState(state);
+	 
+    }
+
+    /** Constructor for a format-string format command.
+     * @param feature The <Strong>Object</strong> this format affects.
+     * @param part The specific part of the control to format as a <strong>String</strong>.
+     * @param value The format <strong>String</strong> which may be a label name, or a piece of HTML formatting.
+     */
+    public Format(Object feature, String part, String value) {
+
+	element = CollectionConfiguration.createElement(StaticStrings.FORMAT_ELEMENT);       
+	setName(feature, part);
+	setValue(value);
+	 
+    }
+
+     
+ 
+    public boolean canHavePart() {
+	return canHavePart(getFeatureName());
+    }
+
+    public int compareTo(Object object) {
+	if(object == null) { // It seems that occasionally compareTo is called and somehow null ends up in comparison.
+	    return -1;
+	}
+	if(object instanceof Format && element != null) {
+	    return element.getAttribute(StaticStrings.NAME_ATTRIBUTE).compareTo(((Format)object).getName());
+	}
+	return toString().compareTo(object.toString());
+    }
+
+    public DOMProxyListEntry create(Element element) {
+	return new Format(element);
+    }
+
+    public boolean equals(Object object) {
+	return (compareTo(object) == 0);
+    }
+
+    public Element getElement() {
+	return element;
+    }
+
+    /** Method to retrieve the value of feature, which may either be a String or a Classifier.
+     * @return The value of feature as an <strong>Object</strong>.
+     */
+    public Object getFeature() {
+	if(classifier != null) {
+	    return classifier;
+	}
+	else if(element != null) {
+	    String feature = getFeatureName();
+	    // If the feature now refers to a classifier, retrieve it.
+	    if(feature.toUpperCase().startsWith(Classifier.CLASSIFIER_PREFIX)) {
+		// Extract the integer index
+		int index = Integer.parseInt(feature.substring(Classifier.CLASSIFIER_PREFIX.length()));
+		// Subtract one (as java is 0-2 where G2 is 1-3)
+		index = index - 1;
+		// Retrieve the appropriate classifier
+		classifier = CollectionDesignManager.classifier_manager.getClassifier(index);
+		return classifier;
+	    }
+	    else {
+		return feature;
+	    }
+	}
+	else {
+	    return "Error";
+	}
+    }
+
+    public String getFeatureName() {
+	if (feature_name == null) {
+	    String name  = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+	    // Remove part
+	    String part = getPart();
+	    if(part != null) {
+		feature_name = name.substring(0, name.length() - part.length());
+	    }
+	    else {
+		feature_name = name;
+	    }
+	    part = null;
+	    name = null;
+	}
+
+	return feature_name;
+	    
+    }
+    public String getName() {
+	if(element != null) {
+	    String name  = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+           return element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+	}
+	return "Error";
+    }
+
+    /** Method to retrieve the value of part. Of course there may not be one, in which case return ""
+     * @return The value of part as a <Strong>String</strong>.
+     */
+    public String getPart() {
+	if(part == null && element != null) {
+	    // To determine a part, we retrieve the Format name, then look for one of our recognized parts
+	    String name = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+	    // DEFAULT_PARTS[0] is an empty string to correctly enable comboboxes.
+	    name = name.toLowerCase();
+	    for(int i = 1; part == null && i < DEFAULT_PARTS.length; i++) {
+		if(name.endsWith(DEFAULT_PARTS[i].toLowerCase())) {
+		    part = DEFAULT_PARTS[i];
+		}
+	    }
+	    if(part == null) {
+		part = DEFAULT_PARTS[0];
+	    }
+	}
+	return part;
+    }
+
+    /** Method to retrieve the value of state.
+     * @return value of state as a <i>boolean</i>.
+     */
+    public boolean getState() {
+	return (element != null && element.getAttribute(StaticStrings.VALUE_ATTRIBUTE).equals(StaticStrings.TRUE_STR));
+    }
+
+    /** Retrieve the value of value.
+     * @return A <strong>String</strong> which is the value of value.
+     */
+    public String getValue() {
+	String value = "Error";
+	if(element != null) {
+	   value = XMLTools.getValue(element);
+	   // Decode into text
+	   value = Codec.transform(value, Codec.DOM_TO_TEXT);
+	}
+	return value;
+    }
+
+    public boolean isAssigned() {
+	return (element != null && !element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR));
+    }
+
+    public boolean isParamType() {
+	return (element != null && element.getAttribute(StaticStrings.VALUE_ATTRIBUTE).length() > 0);
+    }
+
+    public void setAssigned(boolean assigned) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	}
+    }
+
+    public void setElement(Element element) {
+	this.element = element;
+	part = null;
+	text = null;
+	// Establish the classifier reference, if necessary
+	getFeature();
+    }
+
+    public void setName(Object feature, String part) {
+	// Can't do anything unless an element exists.
+        
+	if(element != null) {
+	    this.part = part; // Easier for later on.
+	    if(feature instanceof Classifier) {
+		classifier = (Classifier) feature;
+		feature_name = classifier.getPositionString(); 
+		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, classifier.getPositionString() + part);
+	    }
+	    else {
+                if  (feature.toString().startsWith("<html>")){
+		    feature_name = "";
+		}
+		else{
+		    feature_name = feature.toString();
+		}
+		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, feature_name + part);
+	    }
+	    text = null;
+	}
+    }
+
+    /** Set the value of state.
+     * @param state The new value for state as a <i>boolean</i>.
+     */
+    public void setState(boolean state) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.VALUE_ATTRIBUTE, (state ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	    text = null;
+	}
+    }
+
+    /** Set the value of value. Hmmm.
+     * @param value The new value from value as a <strong>String</strong>.
+     */
+    public void setValue(String value) {
+	if(element != null) {
+	    // Strip off any enclosing speech marks
+	    if(value.startsWith(StaticStrings.SPEECH_CHARACTER) && value.startsWith(StaticStrings.SPEECH_CHARACTER)) {
+		value = value.substring(1, value.length() - 1);
+	    }
+	    // Encode
+	    value = Codec.transform(value, Codec.TEXT_TO_DOM);
+	    // Store in DOM
+	    XMLTools.setValue(element, value);
+	    text = null;
+	}
+    }
+
+    /** Method to translate this classes information into a line of text as you would expect in the collection configuration file.
+     * @return A <strong>String</strong> containing the format command.
+     */
+    public String toString() {
+	if(text == null && element != null) {
+	    StringBuffer temp = new StringBuffer(" ");
+	    temp.append(element.getAttribute(StaticStrings.NAME_ATTRIBUTE));
+	    temp.append(" ");
+	    String value = element.getAttribute(StaticStrings.VALUE_ATTRIBUTE);
+	    if(value.length() > 0) {
+		temp.append(value);
+	    }
+	    else {
+		value = XMLTools.getValue(element);
+		if(value.equalsIgnoreCase(StaticStrings.TRUE_STR) || value.equalsIgnoreCase(StaticStrings.FALSE_STR)) {
+		    temp.append(value);
+		}
+		else {
+		    temp.append("\"");
+		    // Retrieve the DOM encoded value, decode and then append
+		    value = Codec.transform(value, Codec.DOM_TO_TEXT);
+		    temp.append(value);
+		    temp.append("\"");
+		}
+	    }
+	    text = temp.toString();
+	    temp = null;
+	}
+	return text;
+    }
+
+    public void update() {
+	if(classifier != null) {
+	    element.setAttribute(StaticStrings.NAME_ATTRIBUTE, classifier.getPositionString() + getPart());
+	    feature_name = classifier.getPositionString(); 
+	    text = null;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Format4gs3.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Format4gs3.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Format4gs3.java	(revision 31635)
@@ -0,0 +1,327 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Xiao F. Yu, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataTools;
+
+/**
+ * This class is the greenstone 3 version of the Format.java in gs2 for handling
+ * 'gsf' format statements.
+ */
+public class Format4gs3 implements DOMProxyListEntry
+{
+
+	/** The element this format is based on. */
+	private Element element = null;
+	/** We keep a copy of the feature name */
+	private String feature_name = null;
+	/**
+	 * The format statement for this feature (also contains classifier options
+	 * if its a classifier), used by the format list on the top in GLI.
+	 */
+	private String feature_format = null;
+
+	/**
+	 * The format statement for this feature (does not contain options in the
+	 * front), displayed in the format editor at the bottom in GLI. If this
+	 * format is not a classifier, feature_format==pure_format
+	 */
+	private String pure_format = null;
+
+	/**
+	 * Wether this feature is about a classifier. The other way to check this is
+	 * to see if feature_name starts with "CL"
+	 */
+	private static boolean is_classifier = false;
+	/** If this format is about a classifier, this contains all its options */
+	private String classifier_options = "";
+
+	/** Constructor only to be used during DOMProxyListModel initialization. */
+	public Format4gs3()
+	{
+
+	}
+
+	/**
+	 * Constructor for a format-string format command.
+	 * 
+	 * @param element
+	 *            The <Strong>element</strong> this format object contains.
+	 */
+	public Format4gs3(Element element)
+	{
+		this.element = element;
+
+		is_classifier = element.getParentNode().getNodeName().equals(StaticStrings.CLASSIFY_ELEMENT);
+
+		if (is_classifier && CollectionDesignManager.classifier_manager != null)
+		{
+			//The getClassifierPosition() method also sets the classifier_options variable.
+			feature_name = Classifier.CLASSIFIER_PREFIX + getClassifierPosition(element);
+			Classifier classifier = getClassifier(feature_name);
+			//set the classifier_options string
+			if (classifier != null)
+			{
+				classifier_options = classifier.toString();
+			}
+
+			pure_format = toOneLineFormat(XMLTools.getNodeText(element));
+			feature_format = classifier_options + " " + pure_format;
+		}
+		else
+		{
+			feature_name = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			feature_format = toOneLineFormat(XMLTools.getNodeText(element));
+			pure_format = feature_format;
+		}
+
+	}
+
+	//
+	public Format4gs3(String feature_name, String feature_format)
+	{
+		this.feature_name = feature_name;
+		this.feature_format = feature_format;
+		this.pure_format = feature_format;
+
+		is_classifier = feature_name.startsWith(Classifier.CLASSIFIER_PREFIX);
+
+		element = CollectionConfiguration.createElement(StaticStrings.FORMAT_STR);
+		if (is_classifier)
+		{
+
+			//Now get the options of the classifier
+			Classifier classifier = getClassifier(feature_name);
+			//set the classifier_options string
+			classifier_options = classifier.toString();
+
+			feature_format = classifier_options + " " + feature_format;
+		}
+		else
+		{
+			element.setAttribute(StaticStrings.NAME_ATTRIBUTE, feature_name);
+		}
+
+		XMLTools.setNodeText(element, toOneLineFormat(pure_format));
+
+	}
+
+	public int getClassifierPosition(Element element)
+	{
+		//we could also here use 'element.getParentNode().getParentNode()' to get this root node
+		Element root = CollectionDesignManager.collect_config.getDocumentElement();
+		NodeList classify_list = root.getElementsByTagName(StaticStrings.CLASSIFY_ELEMENT);
+		int num_classify_elements = classify_list.getLength();
+		for (int i = 0; i < num_classify_elements; i++)
+		{
+			Element classify = (Element) classify_list.item(i);
+			Element format = (Element) XMLTools.getChildByTagName(classify, StaticStrings.FORMAT_STR);
+			if (format == element)
+			{
+				//Now get the options of the classifier
+				Classifier classifier = CollectionDesignManager.classifier_manager.getClassifier(i);
+				classifier_options = classifier.toString();
+				// i+1 because java is from 0 up, greenstone starts from 1.
+				return i + 1;
+			}
+		}
+		return -1;
+	}
+
+	public static Classifier getClassifier(String feature_name)
+	{
+		int index = Integer.parseInt(feature_name.substring(Classifier.CLASSIFIER_PREFIX.length()));
+		Classifier classifier = CollectionDesignManager.classifier_manager.getClassifier(index - 1);
+		return classifier;
+	}
+
+	//This method is used when the add button is clicked and a new format element for a classifier is added to
+	// the DOM tree, we need to figure out which Classify element to append this format element to.
+	public Element getClassifyElement()
+	{
+
+		//String name = classifier.getPositionString();
+		int classifier_index = Integer.parseInt(feature_name.substring(Classifier.CLASSIFIER_PREFIX.length()));
+		Element root = CollectionDesignManager.collect_config.getDocumentElement();
+		NodeList classify_list = root.getElementsByTagName(StaticStrings.CLASSIFY_ELEMENT);
+
+		// - 1 because java is from 0 up, greenstone starts from 1.
+		classifier_index = classifier_index - 1;
+		return (Element) classify_list.item(classifier_index);
+	}
+
+	/**
+	 * Method to translate this class information into a line of text as you
+	 * would expect on the format list panel
+	 * 
+	 * @return A <strong>String</strong> containing the format command.
+	 */
+	public String toString()
+	{
+
+		return feature_name + " " + toOneLineFormat(feature_format);
+	}
+
+	public void update()
+	{
+
+		if (is_classifier)
+		{
+			feature_name = Classifier.CLASSIFIER_PREFIX + getClassifierPosition(element);
+			Classifier classifier = getClassifier(feature_name);
+			//update the classifier_options string
+			classifier_options = classifier.toString();
+			pure_format = toOneLineFormat(XMLTools.getNodeText(element));
+			feature_format = classifier_options + " " + pure_format;
+		}
+	}
+
+	public String getFormatedFormat()
+	{
+		return toFormatedFormat(feature_format);
+	}
+
+	public String getPureFormat()
+	{
+		return toFormatedFormat(pure_format);
+	}
+
+	public Classifier getClassifier()
+	{
+		if (!isClassifier())
+		{
+
+			return null;
+		}
+		int index = Integer.parseInt(feature_name.substring(Classifier.CLASSIFIER_PREFIX.length()));
+		//index-1 because java indexes from 0, but greenstone is from 1.
+		index = index - 1;
+		return CollectionDesignManager.classifier_manager.getClassifier(index);
+	}
+
+	public void setFeatureName(String name)
+	{
+		feature_name = name;
+	}
+
+	/**
+	 * Set the value of value. Hmmm.
+	 * 
+	 * @param value
+	 *            The new value from value as a <strong>String</strong>.
+	 */
+	public void setFeatureFormat(String value)
+	{
+		if (is_classifier)
+		{
+			feature_format = classifier_options + " " + value;
+		}
+		else
+		{
+			feature_format = value;
+		}
+	}
+
+	public void setPureFormat(String value)
+	{
+		pure_format = value;
+		setFeatureFormat(value);
+		XMLTools.setNodeText(element, pure_format);
+	}
+
+	public static String toOneLineFormat(String str)
+	{
+		
+		return str.replaceAll("\n", "").replaceAll("\\s+", " ").replaceAll(" <", "<");
+	}
+
+	public static String toFormatedFormat(String str)
+	{
+		return str.replaceAll("\\s+", " ").replaceAll("<", "\n<").replaceFirst("\n", "");
+	}
+
+	public int compareTo(Object object)
+	{
+		if (object == null)
+		{ // It seems that occasionally compareTo is called and somehow null ends up in comparison.
+			return -1;
+		}
+		return this.getFeatureName().compareToIgnoreCase(((Format4gs3) object).getFeatureName());
+	}
+
+	public DOMProxyListEntry create(Element element)
+	{
+		return new Format4gs3(element);
+	}
+
+	public boolean equals(Object object)
+	{
+		return (compareTo(object) == 0);
+	}
+
+	public Element getElement()
+	{
+		return element;
+	}
+
+	public boolean isClassifier()
+	{
+		return feature_name.startsWith(Classifier.CLASSIFIER_PREFIX);
+	}
+
+	public String getFeatureName()
+	{
+		return feature_name;
+	}
+
+	//The format appended as a row of string used to display in the format list panel
+	public String getFeatureFormat()
+	{
+		return feature_format;
+	}
+
+	public void setElement(Element element)
+	{
+		this.element = element;
+	}
+
+	public boolean isAssigned()
+	{
+		return true;
+	}
+
+	public void setAssigned(boolean assigned)
+	{
+	}
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Format4gs3Manager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Format4gs3Manager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Format4gs3Manager.java	(revision 31635)
@@ -0,0 +1,971 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.undo.*;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.fife.ui.rsyntaxtextarea.*;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.FormatPane;
+import org.greenstone.gatherer.gui.NumberedJTextArea;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+import org.xml.sax.InputSource;
+
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Insets;
+import java.awt.Rectangle;
+import java.io.ByteArrayInputStream;
+
+/**
+ * This class maintains a list of format statements, and allows the addition and
+ * removal of these statements. This is the greenstone 3 equivalent class of
+ * FormatManager.java which is used by greenstone 2
+ */
+public class Format4gs3Manager implements SharedByTwoFormatManager
+{
+	// The default global format
+	static final private String GLOBAL_FORMAT;
+	static
+	{		
+		// @formatter:off
+		String GLOBAL_FORMAT_TEMP = "" +
+		"<gsf:template name=\"choose-title\">" +
+			"<gsf:choose-metadata>" +
+				"<gsf:metadata name=\"dc.Title\"/>" +
+				"<gsf:metadata name=\"exp.Title\"/>" +
+				"<gsf:metadata name=\"ex.dc.Title\"/>" +
+				"<gsf:metadata name=\"Title\"/>" +
+				"<gsf:default>Untitled</gsf:default>" +
+			"</gsf:choose-metadata>" +
+		"</gsf:template>";
+		GLOBAL_FORMAT = GLOBAL_FORMAT_TEMP;
+		// @formatter:on
+	}
+	static final private String GLOBAL = "global";
+	//The default search format
+	static final private String SEARCH_FORMAT;
+	static
+	{
+		// @formatter:off
+		String SEARCH_FORMAT_TEMP = "" +
+		"<gsf:template match=\"documentNode\">" +
+			"<td valign=\"top\">" +
+				"<gsf:link type=\"document\">" +
+					"<gsf:icon type=\"document\"/>" +
+				"</gsf:link>" +
+			"</td>" +
+			"<td>" +
+				"<gsf:link type=\"document\">" +
+					"<xsl:call-template name=\"choose-title\"/>" +
+				"</gsf:link>" +
+			"</td>" +
+		"</gsf:template>";
+		SEARCH_FORMAT = SEARCH_FORMAT_TEMP;
+		// @formatter:on
+	}
+	static final private String SEARCH = "search";
+	static final private String DISPLAY_DEFAULT_FORMAT;
+	static
+	{
+		// @formatter:off
+		String DISPLAY_DEFAULT_FORMAT_TEMP = "" +
+		"<gsf:option name=\"TOC\" value=\"true\"/>" +
+		"<gsf:option name=\"AllowUserComments\" value=\"false\"/>" +
+		"<!--" +
+			"Overwriting this template allows you to change the heading of the document." +
+		"-->" +
+		"<!--" +
+		"<gsf:template name=\"documentHeading\">" +
+			"<span style=\"font-weight:bold; font-size: 120%;\">" +
+				"<xsl:call-template name=\"choose-title\"/>" +
+			"</span>" +
+		"</gsf:template>" +
+		"-->" +
+		"<!--" +
+			"Overwriting this template can be used to redefine the content of the whole document." + 
+			"This is useful for simple documents, but not recommended for more complex documents" +
+			"(e.g. hierachical and paged documents) as it can prevent any sub-sections from showing." +
+		"-->" +
+		"<!--" +
+		"<gsf:template name=\"documentContent\">" +
+			"<xsl:call-template name=\"wrappedSectionImage\"/>" +
+			"<xsl:call-template name=\"wrappedSectionText\"/>" +
+		"</gsf:template>" +
+		"-->" +
+		"<!--" +
+			"Overwriting this template can be used to change the content of section headings." +
+		"-->" +
+		"<!--" +
+		"<gsf:template name=\"sectionHeading\">" +
+			"<xsl:call-template name=\"choose-title\"/>" +
+		"</gsf:template>" +
+		"-->" +
+		"<!--" +
+			"Overwriting this template can be used to change the content of the top-level section." +
+		"-->" +
+		"<!--" +
+		"<gsf:template name=\"topLevelSectionContent\">" +
+			"<xsl:call-template name=\"wrappedSectionImage\"/>" +
+			"<xsl:call-template name=\"wrappedSectionText\"/>" +
+		"</gsf:template>" +
+		"-->" +
+		"<!--" +
+			"Overwriting this template can be used to change the content of sections." +
+		"-->" +
+		"<!--" +
+		"<gsf:template name=\"sectionContent\">" +
+			"<xsl:call-template name=\"wrappedSectionImage\"/>" +
+			"<xsl:call-template name=\"wrappedSectionText\"/>" +
+		"</gsf:template>" +
+		"-->";
+		// @formatter:on
+		
+		DISPLAY_DEFAULT_FORMAT = DISPLAY_DEFAULT_FORMAT_TEMP;
+	}
+	static final private String DISPLAY = "display";
+
+	//The default browse format
+	static final private String CLASSIFIER_DEFAULT_FORMAT;
+	static
+	{
+		// @formatter:off
+		String CLASSIFIER_DEFAULT_FORMAT_TEMP = "" +
+		"<gsf:template match=\"documentNode\">" + 
+			"<td valign=\"top\">" + 
+				"<gsf:link type=\"document\">" +
+					"<gsf:icon type=\"document\"/>" +
+				"</gsf:link>" +
+			"</td>" +
+			"<td valign=\"top\">" +
+				"<gsf:link type=\"source\">" +
+					"<gsf:choose-metadata>" +
+						"<gsf:metadata name=\"thumbicon\"/>" +
+						"<gsf:metadata name=\"srcicon\"/>" +
+					"</gsf:choose-metadata>" +
+				"</gsf:link>" +
+			"</td>" +
+			"<td valign=\"top\">" +
+				"<gsf:link type=\"document\">" +
+					"<xsl:call-template name=\"choose-title\"/>" +
+				"</gsf:link>" +
+				"<gsf:switch>" +
+					"<gsf:metadata name=\"Source\"/>" +
+					"<gsf:when test=\"exists\"><br/><i>(<gsf:metadata name=\"Source\"/>)</i></gsf:when>" +
+				"</gsf:switch>" +
+			"</td>" +
+		"</gsf:template>" +
+		"<gsf:template match=\"classifierNode[@classifierStyle = 'VList']\">" +
+			"<td valign=\"top\">" +
+				"<gsf:link type=\"classifier\" style=\"static\">" +
+					"<gsf:icon type=\"classifier\"/>" +
+				"</gsf:link>" +
+			"</td>" +
+			"<td valign=\"top\">" +
+		            "<gsf:link type=\"classifier\">" +
+				"<gsf:metadata name=\"Title\"/>" +
+                            "</gsf:link>" +
+			"</td>" +
+		"</gsf:template>" +
+		"<gsf:template match=\"classifierNode[@classifierStyle = 'HList']\">" +
+			"<gsf:link type=\"classifier\">" +
+				"<gsf:metadata name=\"Title\"/>" +
+			"</gsf:link>" +
+		"</gsf:template>";
+		CLASSIFIER_DEFAULT_FORMAT = CLASSIFIER_DEFAULT_FORMAT_TEMP;
+		// @formatter:on
+	}
+	static final private String CLASSIFIER_DEFAULT = "browse";
+	static final private String SEARCHTYPE_FORMAT = "plain,simpleform,advancedform";
+	static final private String SEARCHTYPE = "searchType";
+	static final private String[] FEATURE_NAME = { SEARCH, GLOBAL, DISPLAY, CLASSIFIER_DEFAULT, SEARCHTYPE };
+	static final private String[] FEATURE_FORMAT = { SEARCH_FORMAT, GLOBAL_FORMAT, DISPLAY_DEFAULT_FORMAT, CLASSIFIER_DEFAULT_FORMAT, SEARCHTYPE_FORMAT };
+
+	static private HashMap default_format_map = null;
+	static private HashMap default_format_formated_map = null;
+
+	/** The controls used to edit the format commands. */
+	private Control controls = null;// an interface
+	/** A reference */
+	private DOMProxyListModel format_list_model = null;
+
+	//private DOMProxyListModel feature_list_model = null;
+
+	/** Constructor. */
+	public Format4gs3Manager()
+	{//pass the internal structure
+		Element root = CollectionDesignManager.collect_config.getDocumentElement();
+		format_list_model = new DOMProxyListModel(root, StaticStrings.FORMAT_STR, new Format4gs3());
+		initDefault(format_list_model, FEATURE_NAME, FEATURE_FORMAT);
+		initFormatMap(FEATURE_NAME, FEATURE_FORMAT);
+
+	}
+
+	private void initFormatMap(String[] feature_name, String[] feature_format)
+	{
+		default_format_map = new HashMap();
+		default_format_formated_map = new HashMap();
+		for (int i = 0; i < feature_name.length; i++)
+		{
+			default_format_map.put(feature_name[i], feature_format[i]);
+			default_format_formated_map.put(feature_name[i], Format4gs3.toFormatedFormat(feature_format[i]));
+		}
+
+	}
+
+	public void initDefault(DOMProxyListModel model, String[] feature_name, String[] feature_format)
+	{
+		// Establish all of the format objects.
+		for (int i = 0; i < model.getSize(); i++)
+		{
+			model.getElementAt(i);//get those objects cached
+		}
+		for (int i = 0; i < feature_name.length; i++)
+		{
+			if (getFormat(model, feature_name[i]) == null)
+			{
+				model.add(new Format4gs3(feature_name[i], feature_format[i]));
+
+			}
+		}
+	}
+
+	/**
+	 * Method to remove a format.
+	 * 
+	 * @param format
+	 *            The <strong>Format</strong> to remove.
+	 */
+	private void removeFormat(DOMProxyListModel model, Format4gs3 format)
+	{
+		model.remove(format);
+	}
+
+	private Format4gs3 getFormat(DOMProxyListModel model, String name)
+	{
+
+		for (int index = 0; index < model.getSize(); index++)
+		{
+			Format4gs3 format = (Format4gs3) model.getElementAt(index);
+			if (format.getFeatureName().equals(name))
+			{
+				return format;
+			}
+		}
+		return null;
+	}
+
+	private void addFormat(Element parent, Format4gs3 format)
+	{
+		if (!format_list_model.contains(format))
+		{
+
+			format_list_model.add(parent, format, null);
+		}
+	}
+
+	public void destroy()
+	{
+		if (controls != null)
+		{
+			controls.destroy();
+			controls = null;
+		}
+	}
+
+	/**
+	 * Method to retrieve this managers controls.
+	 * 
+	 * @return the Control for this collection.
+	 */
+	public Control getControls()
+	{
+		if (controls == null)
+		{
+			controls = new FormatControl();
+		}
+		//controls.gainFocus();
+		return controls;
+	}
+
+	/**
+	 * Called when the detail mode has changed which in turn may cause several
+	 * design elements to be available/hidden
+	 * 
+	 * @param mode
+	 *            the new mode as an int
+	 */
+	public void modeChanged(int mode)
+	{
+
+	}
+
+	/** updates the format and feature model */
+	public synchronized void refresh()
+	{
+		for (int i = 0; i < format_list_model.getSize(); i++)
+		{
+			Format4gs3 format = (Format4gs3) format_list_model.getElementAt(i);
+			format.update();
+			format = null;
+		}
+		//call the gainFocus() and in turn the buildFeatureModel() method to get the feature combobox refreshed as well
+		if (controls == null)
+		{
+			controls = new FormatControl();
+		}
+		controls.gainFocus();
+
+		//format_list_model.refresh(); //this call is not necessary as it is included in gainFocus()
+	}
+
+	private ArrayList buildFeatureModel()
+	{
+		// Rebuild feature model.
+		ArrayList feature_model = new ArrayList();
+		//This will display 'choose a feature' and is used when the format feature panel is first gained focus
+		feature_model.add(new Entry(""));
+
+		for (int i = 0; i < format_list_model.getSize(); i++)
+		{
+			Format4gs3 format = (Format4gs3) format_list_model.getElementAt(i);
+			String feature_name = format.getFeatureName();
+			if (!feature_name.startsWith(Classifier.CLASSIFIER_PREFIX))
+			{
+				feature_model.add(new Entry(format.getFeatureName()));
+
+			}
+		}
+		// Now the classifiers.
+		Element root = CollectionDesignManager.collect_config.getDocumentElement();
+		NodeList classifier_list = root.getElementsByTagName(StaticStrings.CLASSIFY_ELEMENT);
+		for (int j = 0; j < classifier_list.getLength(); j++)
+		{
+			feature_model.add(new Entry(CollectionDesignManager.classifier_manager.getClassifier(j)));
+
+		}
+		//Collections.sort (feature_model);
+		return feature_model;
+	}
+
+	private String addSurroundingTags(String xml)
+	{
+		return "<ROOTELEMENT>" + xml + "</ROOTELEMENT>";
+	}
+
+	private String removeSurroundingTags(String xml)
+	{
+	  return xml.replace("<ROOTELEMENT>\n", "").replace("<ROOTELEMENT>", "").replace("</ROOTELEMENT>", "").replace("<ROOTELEMENT/>","");
+	}
+
+	public class FormatControl extends JPanel implements Control
+	{
+
+		private ArrayList feature_model;
+		private boolean ignore_event = false;
+		private boolean ready = false; // Are these controls available to be refreshed
+		private JButton add_button;
+		private JButton remove_button;
+		private JButton default_button;
+		private JComboBox feature_combobox;
+		private JList format_list;
+		private NumberedJTextArea editor_textarea;
+		private JTextArea editor_msgarea;
+		private JPanel validation_msg_panel;
+		private JPanel selection_pane;
+		private final Dimension FIELD_SIZE = new Dimension(200, 30);
+		private boolean newtext = true;
+		private Format4gs3 previousFormat = null;
+		private Format4gs3 currentFormat = null;
+		private boolean fresh = true;
+
+		public FormatControl()
+		{
+			feature_model = buildFeatureModel();
+
+			// Create
+			JPanel header_pane = new DesignPaneHeader("CDM.GUI.Formats", "formatstatements");
+
+			format_list = new JList(format_list_model);
+
+			selection_pane = new JPanel();
+			JPanel feature_pane = new JPanel();
+			JLabel feature_label = new JLabel(Dictionary.get("CDM.FormatManager.Feature"));
+
+			feature_combobox = new JComboBox(feature_model.toArray());
+			feature_combobox.setOpaque(!Utility.isMac());
+			feature_combobox.setPreferredSize(FIELD_SIZE);
+			feature_combobox.setEditable(false);
+			feature_combobox.setToolTipText(Dictionary.get("CDM.FormatManager.Feature_Tooltip"));
+
+			JPanel center_pane = new JPanel();
+			JPanel editor_pane = new JPanel();
+
+			// NumberedJTextArea comes with undo and redo buttons already hooked up to listeners
+			editor_textarea = new NumberedJTextArea(Dictionary.get("CDM.FormatManager.Add_Tooltip"));
+
+			default_button = new GLIButton(Dictionary.get("CDM.FormatManager.Default"), Dictionary.get("CDM.FormatManager.Default_Tooltip"));
+			JPanel button_pane = new JPanel();
+			add_button = new GLIButton(Dictionary.get("CDM.FormatManager.Add"), Dictionary.get("CDM.FormatManager.Add_Tooltip"));
+			add_button.setEnabled(false);
+
+			remove_button = new GLIButton(Dictionary.get("CDM.FormatManager.Remove"), Dictionary.get("CDM.FormatManager.Remove_Tooltip"));
+			remove_button.setEnabled(false);
+
+			// Connect
+			add_button.addActionListener(new AddListener());
+			remove_button.addActionListener(new RemoveListener());
+			default_button.addActionListener(new DefaultListener());
+			feature_combobox.addActionListener(new FeatureListener());
+			editor_textarea.getDocument().addDocumentListener(new EditorListener());
+
+			format_list.addListSelectionListener(new FormatListListener());
+
+			// Layout
+			JPanel format_list_pane = new JPanel();
+			format_list_pane.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));
+			format_list_pane.setLayout(new BorderLayout());
+			format_list_pane.add(new JScrollPane(format_list), BorderLayout.CENTER);
+
+			feature_pane.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));
+			feature_pane.setLayout(new BorderLayout(5, 0));
+			feature_pane.add(feature_label, BorderLayout.WEST);
+			feature_pane.add(feature_combobox, BorderLayout.CENTER);
+
+			JPanel rupanel = new JPanel();
+			rupanel.setLayout(new GridLayout(1, 2));
+			rupanel.add(editor_textarea.undoButton);
+			rupanel.add(editor_textarea.redoButton);
+
+			editor_pane.setLayout(new BorderLayout());
+			editor_pane.add(new JScrollPane(editor_textarea), BorderLayout.CENTER);
+
+			validation_msg_panel = new JPanel();
+			JLabel validation_label = new JLabel(Dictionary.get("CDM.FormatManager.MessageBox"));
+			editor_msgarea = new JTextArea();
+
+			editor_msgarea.setCaretPosition(0);
+			editor_msgarea.setLineWrap(true);
+			editor_msgarea.setRows(3);
+			editor_msgarea.setWrapStyleWord(false);
+			editor_msgarea.setEditable(false);
+			editor_msgarea.setToolTipText(Dictionary.get("CDM.FormatManager.MessageBox_Tooltip"));
+			validation_msg_panel.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
+			validation_msg_panel.setLayout(new BorderLayout(5, 0));
+			validation_msg_panel.add(validation_label, BorderLayout.WEST);
+			validation_msg_panel.add(new JScrollPane(editor_msgarea), BorderLayout.CENTER);
+
+			selection_pane.setLayout(new BorderLayout());
+			selection_pane.add(validation_msg_panel, BorderLayout.NORTH);
+			selection_pane.add(rupanel, BorderLayout.SOUTH);
+			selection_pane.add(editor_pane, BorderLayout.CENTER);
+
+			button_pane.setLayout(new GridLayout(1, 3));
+			button_pane.add(add_button);
+			button_pane.add(remove_button);
+			button_pane.add(default_button);
+
+			center_pane.setLayout(new BorderLayout());
+			center_pane.add(feature_pane, BorderLayout.NORTH);
+			center_pane.add(selection_pane, BorderLayout.CENTER);
+			center_pane.add(button_pane, BorderLayout.SOUTH);
+
+			JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+			splitPane.add(format_list_pane, JSplitPane.TOP);
+			splitPane.add(center_pane, JSplitPane.BOTTOM);
+			splitPane.setDividerLocation(150);
+
+			setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));
+			setLayout(new BorderLayout());
+			add(header_pane, BorderLayout.NORTH);
+			add(splitPane, BorderLayout.CENTER);
+			ready = true;
+		}
+
+		public void destroy()
+		{
+		}
+
+		/**
+		 * Overriden to ensure that the instructions pane is scrolled to the
+		 * top.
+		 */
+		public void gainFocus()
+		{
+			if (ready)
+			{
+
+				format_list_model.refresh();
+				// Update the feature model, trying to maintain the same selected object
+				Object selected_feature = feature_combobox.getSelectedItem();
+				feature_combobox.setSelectedItem(selected_feature);
+				feature_model = buildFeatureModel();
+				feature_combobox.setModel(new DefaultComboBoxModel(feature_model.toArray()));
+			}
+		}
+
+		public void loseFocus()
+		{
+			//validate the templates. If not wellformed, pop up an alert; otherwise, do nothing.
+			String msg = XMLTools.parse(editor_textarea.getText());
+			if (msg.startsWith(XMLTools.NOTWELLFORMED))
+			{
+				JOptionPane.showMessageDialog(null, msg, XMLTools.NOTWELLFORMED, JOptionPane.ERROR_MESSAGE);
+			}
+			format_list_model.refresh();
+		}
+
+		public Format4gs3 getCurrentFormat()
+		{
+			return (Format4gs3) format_list.getSelectedValue();
+
+		}
+
+		/**
+		 * Listens for clicks on the add button, and if the relevant details are
+		 * provided adds a new format. Note that formats are responsible for
+		 * codecing the values into something that can be a) stored in a DOM and
+		 * b) written to file
+		 */
+		private class AddListener implements ActionListener
+		{
+
+			public void actionPerformed(ActionEvent event)
+			{
+
+				ignore_event = true; // Prevent format_list excetera propagating events
+
+				String format_str = editor_textarea.getText();
+				Entry entry = (Entry) feature_combobox.getSelectedItem();
+				String feature_name = entry.getClassifier().getPositionString();
+
+				Format4gs3 format = new Format4gs3(feature_name, format_str);
+				Element e = format.getClassifyElement();
+				addFormat(e, format);
+				existingFormat(format.getFeatureName().startsWith(Classifier.CLASSIFIER_PREFIX)); // set the appropriate enable/disable on the interface
+
+				// Update list selection (make the new added format highlighted on the format list panel)
+				format_list.setSelectedValue(format, true);
+
+				format = null;
+
+				ignore_event = false;
+			}
+		}
+
+		private class EditorListener implements DocumentListener
+		{
+
+			public void changedUpdate(DocumentEvent e)
+			{
+				update();
+			}
+
+			public void insertUpdate(DocumentEvent e)
+			{
+				update();
+				updateUndo("insert");
+
+			}
+
+			public void removeUpdate(DocumentEvent e)
+			{
+				update();
+				updateUndo("remove");
+
+			}
+
+			private void updateUndo(String from)
+			{
+
+				if (!newtext)
+				{
+				    editor_textarea.undoButton.setEnabled(true);
+				}
+
+				if (editor_textarea.getText().length() != 0 && newtext)
+				{
+					newtext = false;
+				}
+			}
+
+			public void update()
+			{
+				if (!format_list.isSelectionEmpty())
+				{
+					Format4gs3 format = (Format4gs3) format_list.getSelectedValue();
+					String format_str = editor_textarea.getText();
+					String msg = XMLTools.parse(format_str);
+					editor_msgarea.setText(msg);
+
+					if (msg.startsWith(XMLTools.WELLFORMED))
+					{
+						format.setPureFormat(format_str);
+						format.update();
+						format_list_model.refresh(format);
+						editor_msgarea.setBackground(Color.white);
+						FormatPane.setPreviewButton(true);
+					}
+					else
+					{
+						editor_msgarea.setBackground(Color.red);
+						FormatPane.setPreviewButton(false);
+					}
+				}
+				else
+				{
+					add_button.setEnabled(false);
+				}
+			}
+		}
+
+		private class FeatureListener implements ActionListener
+		{
+			public void actionPerformed(ActionEvent event)
+			{
+				editor_textarea.undoButton.setEnabled(false);
+				editor_textarea.redoButton.setEnabled(false);
+				default_button.setEnabled(true);
+				newtext = true;
+
+				if (ignore_event == true)
+				{
+				        editor_textarea.discardAllEdits();
+					return;
+				}
+
+				ignore_event = true;
+				// Add is only enabled if there isn't already a format for the choosen feature and part.
+				//Create a dummy format and test if itsa already in the model
+				Entry entry = (Entry) feature_combobox.getSelectedItem();
+				String feature_name = entry.getFeatureName();
+
+				Format4gs3 format = getFormat(format_list_model, feature_name);
+
+				if (format != null)
+				{
+					///ystem.err.println("There is an existing format!");
+					format_list.setSelectedValue(format, true);
+					Element formatElem = null;
+					try
+					{
+						InputSource is = new InputSource(new ByteArrayInputStream(addSurroundingTags(format.getPureFormat()).getBytes("utf-8")));
+						formatElem = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is).getDocumentElement();
+					}
+					catch (Exception ex)
+					{
+						ex.printStackTrace();
+					}
+
+					if (formatElem != null)
+					{
+						StringBuffer sb = new StringBuffer();
+						XMLTools.xmlNodeToString(sb, formatElem, true, "  ", 0);
+						editor_textarea.setText(removeSurroundingTags(sb.toString()));
+					}
+					else
+					{
+						editor_textarea.setText(format.getPureFormat());
+					}
+					editor_textarea.setCaretPosition(0);
+
+					existingFormat(feature_name.startsWith(Classifier.CLASSIFIER_PREFIX));
+				}
+				// Otherwise there is no existing format, then this feature must be a classifier (CL1, 2, ...)
+				// we display the ClassifierDefault for this format
+				else
+				{
+					//Fist reset the format list panel
+					format_list.clearSelection();
+
+					if (feature_name.equals(""))
+					{
+						editor_textarea.setText("");
+
+					}
+					else
+					{
+						//Only for debugging purposes
+						if (entry.getClassifier() == null)
+						{
+							DebugStream.println("It should be a classifier or choose a feature. What is it? " + entry.toString());
+						}
+
+						editor_textarea.setText(Format4gs3.toFormatedFormat(CLASSIFIER_DEFAULT_FORMAT));
+						editor_textarea.setCaretPosition(0);
+						newFormat();
+					}
+				}
+				ignore_event = false;
+				editor_textarea.discardAllEdits();
+			}
+		}
+
+		private class FormatListListener implements ListSelectionListener
+		{
+			public void valueChanged(ListSelectionEvent event)
+			{
+			        editor_textarea.undoButton.setEnabled(false);
+				editor_textarea.redoButton.setEnabled(false);
+				default_button.setEnabled(true);
+				newtext = true;
+
+				if (!ignore_event && !event.getValueIsAdjusting())
+				{
+
+					if (!format_list.isSelectionEmpty())
+					{
+						ignore_event = true;
+						Format4gs3 format = (Format4gs3) format_list.getSelectedValue();
+						String feature_name = format.getFeatureName();
+						Entry entry = null;
+						if (feature_name.startsWith(Classifier.CLASSIFIER_PREFIX))
+						{
+							entry = new Entry(format.getClassifier());
+						}
+						else
+						{
+							entry = new Entry(feature_name);
+						}
+						feature_combobox.setSelectedItem(entry);
+
+						existingFormat(format.getFeatureName().startsWith(Classifier.CLASSIFIER_PREFIX));
+
+						Element formatElem = null;
+						try
+						{
+							InputSource is = new InputSource(new ByteArrayInputStream(addSurroundingTags(format.getPureFormat()).getBytes("utf-8")));
+							formatElem = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is).getDocumentElement();
+						}
+						catch (Exception ex)
+						{
+							ex.printStackTrace();
+						}
+
+						if (formatElem != null)
+						{
+							StringBuffer sb = new StringBuffer();
+							XMLTools.xmlNodeToString(sb, formatElem, true, "  ", 0);
+							editor_textarea.setText(removeSurroundingTags(sb.toString()));
+						}
+						else
+						{
+							editor_textarea.setText(format.getPureFormat());
+						}
+						editor_textarea.setCaretPosition(0);
+
+						ignore_event = false;
+					}
+
+				}
+				editor_textarea.discardAllEdits();
+			}
+
+		}
+
+		private class RemoveListener implements ActionListener
+		{
+			public void actionPerformed(ActionEvent event)
+			{
+				if (!format_list.isSelectionEmpty())
+				{
+					// Remove the current format
+					Format4gs3 format = (Format4gs3) format_list.getSelectedValue();
+					removeFormat(format_list_model, format);
+					// Change buttons
+					add_button.setEnabled(true);
+					feature_combobox.setSelectedItem(new Entry(""));
+					newFormat();
+				}
+			}
+		}
+
+		private class DefaultListener implements ActionListener
+		{
+			public void actionPerformed(ActionEvent event)
+			{
+				newtext = false;
+				if (!ignore_event)
+				{
+					Entry entry = (Entry) feature_combobox.getSelectedItem();
+					String feature_name = entry.getFeatureName();
+					Format4gs3 format = getFormat(format_list_model, feature_name);
+
+					if (format != null)
+					{
+						if (format.isClassifier() == true)
+						{
+							editor_textarea.setText((String) default_format_formated_map.get(CLASSIFIER_DEFAULT));
+							editor_textarea.setCaretPosition(0);
+							remove_button.setEnabled(true);
+						}
+						else
+						{
+							editor_textarea.setText((String) default_format_formated_map.get(format.getFeatureName()));
+							editor_textarea.setCaretPosition(0);
+							remove_button.setEnabled(false);
+						}
+					}
+					else
+					{
+						editor_textarea.setText((String) default_format_formated_map.get(CLASSIFIER_DEFAULT));
+						editor_textarea.setCaretPosition(0);
+						remove_button.setEnabled(false);
+						add_button.setEnabled(true);
+					}
+				}
+			}
+		}
+
+		private void newFormat()
+		{
+			editor_textarea.setEditable(false);
+			editor_textarea.setBackground(Color.lightGray);
+			editor_textarea.setToolTipText(Dictionary.get("CDM.FormatManager.Editor_Disabled_Tooltip"));
+
+			editor_textarea.undoButton.setEnabled(false);
+			editor_textarea.redoButton.setEnabled(false);
+			add_button.setEnabled(true);
+			remove_button.setEnabled(false);
+			default_button.setEnabled(false);
+			FormatPane.setPreviewButton(true);
+		}
+
+		private void existingFormat(boolean enableRemoveButton)
+		{
+			editor_textarea.setEditable(true);
+			editor_textarea.setBackground(Color.white);
+			editor_textarea.setToolTipText(Dictionary.get("CDM.FormatManager.Editor_Tooltip"));
+			add_button.setEnabled(false);
+			remove_button.setEnabled(enableRemoveButton);
+			default_button.setEnabled(true);
+			FormatPane.setPreviewButton(true);
+		}
+	}
+
+	/**
+	 * This object provides a wrapping around an entry in Format4gs3, which is
+	 * tranparent.
+	 */
+	// This class is used for display in the feature combobox
+	private class Entry implements Comparable
+	{
+		private Classifier classifier = null;
+		private String feature_name = null;
+
+		public Entry(Object object)
+		{
+			if (object instanceof Classifier)
+			{
+				classifier = (Classifier) object;
+				feature_name = classifier.getPositionString();
+			}
+			else if (object instanceof String)
+			{
+				feature_name = (String) object;
+			}
+			else
+			{
+				feature_name = "";
+			}
+		}
+
+		public Entry(String text)
+		{
+			this.feature_name = text;
+		}
+
+		public int compareTo(Object object)
+		{
+			if (object == null)
+			{
+				return 1;
+			}
+			if (toString() == null)
+			{
+				return -1;
+			}
+			else
+			{
+				String object_str = object.toString();
+				if (object_str == null)
+				{
+					return 1;
+				}
+				return toString().compareTo(object_str);
+			}
+		}
+
+		public boolean equals(Object object)
+		{
+			if (compareTo(object) == 0)
+			{
+				return true;
+			}
+			return false;
+		}
+
+		public Classifier getClassifier()
+		{
+			return classifier;
+		}
+
+		public String toString()
+		{
+			if (classifier != null)
+			{
+				// Return the classifier name - with its CL index shown, and all its metadata options as well
+				return classifier.getPositionString() + StaticStrings.SPACE_CHARACTER + classifier.toString();
+			}
+			if (feature_name.equals(""))
+			{
+				return "<html><body><i>" + "Choose a feature" + "</i></body></html>";
+			}
+			return feature_name;
+		}
+
+		public String getFeatureName()
+		{
+			return feature_name;
+		}
+	}
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/FormatManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/FormatManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/FormatManager.java	(revision 31635)
@@ -0,0 +1,1289 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.lang.reflect.Field;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.undo.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.WarningDialog;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.w3c.dom.*;
+
+import org.fife.ui.rsyntaxtextarea.*;
+
+
+/** This class maintains a list of format statements, and allows the addition and removal of these statements.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class FormatManager
+  extends DOMProxyListModel implements SharedByTwoFormatManager {
+  
+  static final private String BLANK = "blank";
+  static final private String FLAG  = "flag";
+  static final private String VALUE = "value";
+  
+  static final private String DATELIST = "DateList";
+  static final private String DATELIST_DEFAULT_FORMAT = "<td>[link][icon][/link]</td>\n<td>[highlight]{Or}{[dc.Title],[exp.Title],[ex.Title],Untitled}[/highlight]</td>\n<td>{Or}{[format:dc.Date],[format:exp.Date],[format:ex.Date]}</td>";
+  static final private String HLIST = "HList";
+  static final private String HLIST_DEFAULT_FORMAT = "[link][highlight][ex.Title][/highlight][/link]";
+  static final private String VLIST = "VList";
+  static final private String VLIST_DEFAULT_FORMAT = "<td valign=\"top\">[link][icon][/link]</td>\n<td valign=\"top\">[ex.srclink]{Or}{[ex.thumbicon],[ex.srcicon]}[ex./srclink]</td>\n<td valign=\"top\">[highlight]\n{Or}{[dc.Title],[exp.Title],[ex.Title],Untitled}\n[/highlight]{If}{[ex.Source],<br><i>([ex.Source])</i>}</td>";
+  //    static final private String INVISIBLE = "Invisible";
+  //static final private String INVISIBLE_DEFAULT_FORMAT = "";
+  
+  static final private String DOCUMENTHEADING = "DocumentHeading";
+  static final private String DOCUMENTHEADING_DEFAULT_FORMAT = "{Or}{[parent(Top):Title],[Title],untitled}<br>";
+  
+  static final private String DOCUMENTTEXT = "DocumentText";
+  static final private String DOCUMENTTEXT_DEFAULT_FORMAT = "[Text]";
+  
+  static final private String DOCUMENTBUTTONS = "DocumentButtons";
+  static final private String DOCUMENTBUTTONS_DEFAULT_FORMAT = "Detach|Highlight";
+  static final private String SEARCHTYPES = "SearchTypes";
+  static final private String SEARCHTYPES_DEFAULT_FORMAT = "plain,form";
+  
+  static final private String NAVBAR = "NavigationBar";
+  static final private String NAVBAR_OPTION = "pulldown";
+
+  static private HashMap default_format_map = null;
+  
+  /** The controls used to edit the format commands. */
+  private Control controls = null;
+  /** A reference to ourselves so inner classes can get at the model. */
+  private DOMProxyListModel model = null;
+  
+  
+  /** Constructor. */
+  public FormatManager() {
+    super(CollectionDesignManager.collect_config.getDocumentElement(), StaticStrings.FORMAT_ELEMENT, new Format());
+    this.model = this;
+    
+    default_format_map = new HashMap();
+    default_format_map.put(DATELIST, DATELIST_DEFAULT_FORMAT);
+    default_format_map.put(HLIST, HLIST_DEFAULT_FORMAT);
+    default_format_map.put(VLIST, VLIST_DEFAULT_FORMAT);
+    default_format_map.put(DOCUMENTHEADING, DOCUMENTHEADING_DEFAULT_FORMAT);
+    default_format_map.put(DOCUMENTTEXT, DOCUMENTTEXT_DEFAULT_FORMAT);
+    default_format_map.put(DOCUMENTBUTTONS, DOCUMENTBUTTONS_DEFAULT_FORMAT);
+    default_format_map.put(SEARCHTYPES, SEARCHTYPES_DEFAULT_FORMAT);
+    default_format_map.put(NAVBAR, NAVBAR_OPTION);
+    default_format_map.put("", "");
+    
+    DebugStream.println("FormatManager: parsed " + getSize() + " format statements.");
+    // Establish all of the format objects, so that classifier indexes are initially correct (subsequent refreshes of the model will be sufficient to keep these up to date, as long as we start with a live reference to a classifier.
+    int size = getSize();
+    for(int i = 0; i < size; i++) {
+      getElementAt(i);
+    }
+    
+    // Ensure the default formats for important format statements are assigned
+    if (getFormat(VLIST) == null) {
+      addFormat(new Format("",VLIST,VLIST_DEFAULT_FORMAT));
+    }
+    if (getFormat(HLIST) == null) {
+      addFormat(new Format("",HLIST,HLIST_DEFAULT_FORMAT));
+    }
+    // only if DateList is used
+    if (getFormat(DATELIST) == null && CollectionDesignManager.classifier_manager.isDateListClassifierAssigned()) {
+      addFormat(new Format("",DATELIST,DATELIST_DEFAULT_FORMAT));
+    }
+    if (getFormat(DOCUMENTHEADING) == null) {
+      addFormat(new Format(DOCUMENTHEADING, "", DOCUMENTHEADING_DEFAULT_FORMAT));
+    }
+    if (getFormat(DOCUMENTTEXT)== null) {
+      addFormat(new Format(DOCUMENTTEXT,"",DOCUMENTTEXT_DEFAULT_FORMAT));
+    }
+    if (getFormat(DOCUMENTBUTTONS) == null) {
+      addFormat(new Format(DOCUMENTBUTTONS,"",DOCUMENTBUTTONS_DEFAULT_FORMAT));
+    }
+    // only for mgpp and lucene colls
+    if (getFormat(SEARCHTYPES)==null && (CollectionDesignManager.index_manager.isMGPP()) || CollectionDesignManager.index_manager.isLucene()) {
+      addFormat(new Format(SEARCHTYPES, "", SEARCHTYPES_DEFAULT_FORMAT));
+    }
+    
+    
+  }
+  
+  /** Method to add a new format to this manager.
+   * @param format The <strong>Format</strong> to add.
+   */
+  private void addFormat(Format format) {
+    if(!contains(format)) {
+      Element element = format.getElement();
+      // Locate where we should insert this new classifier.
+      Node target_node = CollectionConfiguration.findInsertionPoint(element);
+      add(root, format, target_node);
+    }
+  }
+  
+  
+  public void destroy() {
+    if(controls != null) {
+      controls.destroy();
+      controls = null;
+    }
+  }
+  
+  
+  private Format getFormat(String name) {
+    int model_size = getSize();
+    for(int index = 0; index < model_size; index++) {
+      Format format = (Format) getElementAt(index);
+      if(format.getName().equals(name)) {
+        return format;
+      }
+    }
+    return null;
+  }
+  
+  public String getDefaultFormatString(String name){
+    String default_str = "";
+    try{
+      String field_name = name.toUpperCase()+"_DEFAULT_FORMAT";    
+      Field field = this.getClass().getDeclaredField(field_name);
+      Class fieldType = field.getType();
+      default_str = (String) field.get(fieldType);
+    } catch (SecurityException e) {
+      //System.err.println("Debug: "+e.getMessage());
+      default_str = VLIST_DEFAULT_FORMAT;
+    } catch (NoSuchFieldException e) {
+      //System.err.println("Debug: "+e.getMessage());
+      default_str = VLIST_DEFAULT_FORMAT;
+    } catch (IllegalArgumentException e) {
+      //System.err.println("Debug: "+e.getMessage());
+      default_str = VLIST_DEFAULT_FORMAT;
+    } catch (IllegalAccessException e) {
+      //System.err.println("Debug: "+e.getMessage());		
+      default_str = VLIST_DEFAULT_FORMAT;
+    }
+    return default_str;
+  }
+  
+  /** Method to retrieve this managers controls.
+   * @return the Control for this collection.
+   */
+  public Control getControls() {
+    if(controls == null) {
+      controls = new FormatControl();
+    }
+    return controls;
+  }
+  
+  /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+   * @param mode the new mode as an int
+   */
+  public void modeChanged(int mode) {
+    
+  }
+  
+  
+  /** updates the format model */
+  public synchronized void refresh() {
+    for(int i = 0; i < getSize(); i++) {
+      Format format = (Format) getElementAt(i);
+      format.update();
+      format = null;
+    }
+    super.refresh();
+  }
+  /** Method to remove a format.
+   * @param format The <strong>Format</strong> to remove.
+   */
+  private void removeFormat(Format format) {
+    remove(format);
+  }
+  
+  private ArrayList buildFeatureModel() {
+    // Rebuild feature model.
+    ArrayList feature_model = new ArrayList();
+    // Add the set options
+    for(int i = 0; i < Format.DEFAULT_FEATURES.length; i++) {
+      feature_model.add(new Entry(Format.DEFAULT_FEATURES[i]));
+    }
+    // Now the classifiers.
+    for(int j = 0; j < CollectionDesignManager.classifier_manager.getSize(); j++) {
+      feature_model.add(new Entry(CollectionDesignManager.classifier_manager.getClassifier(j)));
+    }
+    Collections.sort(feature_model);
+    return feature_model;
+  }
+  
+  private ArrayList buildPartModel() {
+    DebugStream.println("buildPartModel(): replace me with something that reads in a data xml file.");
+    ArrayList part_model = new ArrayList();
+    part_model.add(new Part("", ""));
+    part_model.add(new Part(DATELIST, DATELIST_DEFAULT_FORMAT));
+    part_model.add(new Part(HLIST, HLIST_DEFAULT_FORMAT));
+    //part_model.add(new Part(INVISIBLE, INVISIBLE_DEFAULT_FORMAT));
+    part_model.add(new Part(VLIST, VLIST_DEFAULT_FORMAT));
+    return part_model;
+  }
+    
+  private ArrayList buildVariableModel() {
+    ArrayList variable_model = new ArrayList();
+    variable_model.add(Dictionary.get("CDM.FormatManager.Insert_Variable"));
+    variable_model.add("[Text]");
+    ArrayList every_metadata_set_element = MetadataSetManager.getEveryMetadataSetElement();
+    for (int i = 0; i < every_metadata_set_element.size(); i++) {
+      variable_model.add("[" + ((MetadataElement) every_metadata_set_element.get(i)).getFullName() + "]");
+    }
+    variable_model.add("[link]");
+    variable_model.add("[/link]");
+    variable_model.add("[icon]");
+    variable_model.add("[numleafdocs]");
+    variable_model.add("[num]");
+    variable_model.add("[parent():_]");
+    variable_model.add("[parent(Top):_]");
+    variable_model.add("[parent(All'_'):_]");
+    variable_model.add("[child():_]");
+    variable_model.add("[child(All'_'):_]");
+    variable_model.add("[sibling():_]");
+    variable_model.add("[sibling(All'_'):_]");
+    
+    return variable_model;
+  }
+  
+  public class FormatControl
+    extends JPanel
+    implements Control{
+    
+    private ArrayList feature_model;
+    private ArrayList part_model;
+    private ArrayList variable_model;
+    private boolean ignore_event = false;
+    private boolean ready = false; // Are these controls available to be refreshed
+    private CardLayout card_layout;
+    private JButton add_button;
+    private JButton insert_button;
+    private JButton remove_button;
+    private JButton default_button;
+    private JButton undo_button;
+    private JButton redo_button;
+    private JCheckBox enabled_checkbox;
+    private JComboBox feature_combobox;
+    private JComboBox part_combobox;
+    private JComboBox variable_combobox;
+    private JList format_list;
+    private RSyntaxTextArea editor_textarea;
+    private JPanel blank_pane;
+    private JPanel control_pane;
+    private JPanel part_pane;
+    private JPanel selection_pane;
+    private String view_type;
+    private final Dimension FIELD_SIZE = new Dimension(200, 30);
+    private final UndoManager undo = new UndoManager();
+    private boolean newtext = true;
+    private Format previousFormat = null;
+    private Format currentFormat = null;
+    private boolean fresh = true;
+    
+    public FormatControl() {
+      this.setComponentOrientation(Dictionary.getOrientation());
+      feature_model = buildFeatureModel();
+      part_model = buildPartModel();
+      variable_model = buildVariableModel();
+      
+      // Create
+      JPanel header_pane = new DesignPaneHeader("CDM.GUI.Formats", "formatstatements");
+      
+      format_list = new JList(model);
+      
+      selection_pane = new JPanel();
+      JPanel feature_pane = new JPanel();
+      feature_pane.setComponentOrientation(Dictionary.getOrientation());
+
+      JLabel feature_label = new JLabel(Dictionary.get("CDM.FormatManager.Feature"));
+      feature_label.setComponentOrientation(Dictionary.getOrientation());
+
+      feature_combobox = new JComboBox(feature_model.toArray());
+      feature_combobox.setComponentOrientation(Dictionary.getOrientation());
+
+      feature_combobox.setOpaque(!Utility.isMac());
+      feature_combobox.setPreferredSize(FIELD_SIZE);
+      feature_combobox.setEditable(false);
+      feature_combobox.setToolTipText(Dictionary.get("CDM.FormatManager.Feature_Tooltip"));
+      
+      part_pane = new JPanel();
+      part_pane.setComponentOrientation(Dictionary.getOrientation());
+      JLabel part_label = new JLabel(Dictionary.get("CDM.FormatManager.Part"));
+      part_label.setComponentOrientation(Dictionary.getOrientation());
+      part_combobox = new JComboBox(part_model.toArray());
+      part_combobox.setComponentOrientation(Dictionary.getOrientation());
+      part_combobox.setOpaque(!Utility.isMac());
+      part_combobox.setPreferredSize(FIELD_SIZE);
+      part_combobox.setEditable(false);
+      part_combobox.setToolTipText(Dictionary.get("CDM.FormatManager.Part_Tooltip"));
+      
+      blank_pane = new JPanel();
+      
+      JPanel center_pane = new JPanel();
+      
+      card_layout = new CardLayout();
+      control_pane = new JPanel();
+      
+      JPanel blank_pane = new JPanel();
+      
+      JPanel editor_pane = new JPanel();
+      JPanel editor_header_pane = new JPanel();
+      JLabel editor_label = new JLabel(Dictionary.get("CDM.FormatManager.Editor"));
+      editor_label.setComponentOrientation(Dictionary.getOrientation());
+      
+      editor_textarea = new RSyntaxTextArea();
+
+      /* Fields specific to RSyntaxQuery inherited class */
+      editor_textarea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_HTML);
+      editor_textarea.setBracketMatchingEnabled(true);
+      editor_textarea.setAnimateBracketMatching(true);
+      editor_textarea.setAntiAliasingEnabled(true);
+      editor_textarea.setAutoIndentEnabled(true);
+      editor_textarea.setPaintMarkOccurrencesBorder(false);
+
+      /* Standard fields to JTextArea */
+      editor_textarea.setBackground(Configuration.getColor("coloring.editable_background", false));
+      editor_textarea.setCaretPosition(0);
+      editor_textarea.setLineWrap(true);
+      editor_textarea.setRows(6);
+      editor_textarea.setWrapStyleWord(false);
+      editor_textarea.setToolTipText(Dictionary.get("CDM.FormatManager.Add_Tooltip"));
+      
+      variable_combobox = new JComboBox(variable_model.toArray());
+      variable_combobox.setComponentOrientation(Dictionary.getOrientation());
+      variable_combobox.setOpaque(!Utility.isMac());
+      variable_combobox.setToolTipText(Dictionary.get("CDM.FormatManager.Variable_Tooltip"));
+      
+      insert_button = new GLIButton(Dictionary.get("CDM.FormatManager.Insert"), Dictionary.get("CDM.FormatManager.Insert_Tooltip"));
+      
+      default_button = new GLIButton(Dictionary.get("CDM.FormatManager.Default"), Dictionary.get("CDM.FormatManager.Default_Tooltip"));
+      
+      JPanel flag_pane = new JPanel();
+      enabled_checkbox = new JCheckBox(Dictionary.get("CDM.FormatManager.Enabled"));
+      enabled_checkbox.setComponentOrientation(Dictionary.getOrientation());
+      
+      JPanel button_pane = new JPanel();
+      button_pane.setComponentOrientation(Dictionary.getOrientation());
+      add_button = new GLIButton(Dictionary.get("CDM.FormatManager.Add"), Dictionary.get("CDM.FormatManager.Add_Tooltip"));
+      add_button.setEnabled(false);
+      
+      remove_button = new GLIButton(Dictionary.get("CDM.FormatManager.Remove"), Dictionary.get("CDM.FormatManager.Remove_Tooltip"));
+      remove_button.setEnabled(false);
+      
+      undo_button = new GLIButton(Dictionary.get("General.Undo"), Dictionary.get("General.Undo_Tooltip"));
+      undo_button.setEnabled(false);
+      
+      redo_button = new GLIButton(Dictionary.get("General.Redo"), Dictionary.get("General.Redo_Tooltip"));
+      redo_button.setEnabled(false);
+      
+      // Connect
+      add_button.addActionListener(new AddListener());
+      remove_button.addActionListener(new RemoveListener());
+      default_button.addActionListener(new DefaultListener());
+      undo_button.addActionListener(new UndoListener());
+      redo_button.addActionListener(new RedoListener());
+      enabled_checkbox.addActionListener(new EnabledListener());
+      feature_combobox.addActionListener(new FeatureListener());
+      part_combobox.addActionListener(new PartListener());
+      
+      editor_textarea.getDocument().addDocumentListener(new EditorListener());
+      // Listen for undo and redo events
+      editor_textarea.getDocument().addUndoableEditListener(new UndoableEditListener() {
+        public void undoableEditHappened(UndoableEditEvent evt) {
+          undo.addEdit(evt.getEdit());
+        }
+      });
+      
+      
+      format_list.addListSelectionListener(new FormatListListener());
+      variable_combobox.addActionListener(new VariableListener());
+      
+      // Layout
+      JPanel format_list_pane = new JPanel();
+      format_list_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+      format_list_pane.setLayout(new BorderLayout());
+      format_list_pane.add(new JScrollPane(format_list), BorderLayout.CENTER);
+      
+      
+      feature_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+      feature_pane.setLayout(new BorderLayout(5,0));
+      feature_pane.add(feature_label, BorderLayout.LINE_START);
+      feature_pane.add(feature_combobox, BorderLayout.CENTER);
+      
+      part_pane.setBorder(BorderFactory.createEmptyBorder(2,0,0,0));
+      part_pane.setLayout(new BorderLayout(5, 0));
+      part_pane.add(part_label, BorderLayout.LINE_START);
+      part_pane.add(part_combobox, BorderLayout.CENTER);
+      
+      
+      flag_pane.add(enabled_checkbox);
+      
+      editor_header_pane.setBorder(BorderFactory.createEmptyBorder(2,0,2,0));
+      editor_header_pane.setLayout(new GridLayout(1,3));
+      editor_header_pane.add(editor_label);
+      
+      JPanel rupanel = new JPanel();
+      rupanel.setComponentOrientation(Dictionary.getOrientation());
+      rupanel.setLayout(new GridLayout(1,2));
+      rupanel.add(undo_button);
+      rupanel.add(redo_button);
+      
+      JPanel variable_pane = new JPanel();
+      variable_pane.setComponentOrientation(Dictionary.getOrientation());
+      variable_pane.setBorder(BorderFactory.createEmptyBorder(2,0,2,0));
+      variable_pane.setLayout(new GridLayout(1,3));
+      variable_pane.add(new JPanel());
+      variable_pane.add(variable_combobox);
+      variable_pane.add(rupanel);
+      
+      editor_pane.setLayout(new BorderLayout());
+      editor_pane.add(editor_header_pane, BorderLayout.NORTH);
+      editor_pane.add(new JScrollPane(editor_textarea), BorderLayout.CENTER);
+      
+      selection_pane.setLayout(new BorderLayout());
+      selection_pane.add(part_pane, BorderLayout.NORTH);
+      selection_pane.add(editor_pane, BorderLayout.CENTER);
+      selection_pane.add(variable_pane, BorderLayout.SOUTH);
+      
+      control_pane.setLayout(card_layout);
+      control_pane.add(flag_pane, FLAG);
+      control_pane.add(selection_pane, VALUE);
+      control_pane.add(blank_pane, BLANK);
+      
+      
+      button_pane.setLayout(new GridLayout(1,3));
+      button_pane.add(add_button);
+      button_pane.add(remove_button);
+      button_pane.add(default_button);
+      
+      
+      center_pane.setLayout(new BorderLayout());
+      center_pane.add(feature_pane, BorderLayout.NORTH);
+      center_pane.add(control_pane, BorderLayout.CENTER);
+      center_pane.add(button_pane, BorderLayout.SOUTH);
+      
+      setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+      setLayout(new BorderLayout());
+      add(header_pane, BorderLayout.NORTH);
+      add(format_list_pane, BorderLayout.CENTER);
+      add(center_pane, BorderLayout.SOUTH);
+      ready = true;
+    }
+    
+    public void destroy() {
+    }
+    
+    
+    
+    /** Overriden to ensure that the instructions pane is scrolled to the top.
+     */
+    public void gainFocus() {
+      // This is only necessary if the components have been realized
+      if(ready) {
+        model.refresh();
+        
+        // Update the feature model, trying to maintain the same selected object
+        Object selected_feature = feature_combobox.getSelectedItem();
+        feature_model = buildFeatureModel();
+        feature_combobox.setModel(new DefaultComboBoxModel(feature_model.toArray()));
+        feature_combobox.setSelectedItem(selected_feature);
+        
+        // Update the variable model,
+        variable_model = buildVariableModel();
+        variable_combobox.setModel(new DefaultComboBoxModel(variable_model.toArray()));
+      }
+    }
+    
+    public void loseFocus() {
+      // Force all of the Formats to update their names with the correct values.
+      // do we need to do this on loseFocus???
+      model.refresh();
+    }
+    
+    public Format getCurrentFormat() {
+      return (Format)format_list.getSelectedValue();
+      
+    }
+    
+    /** Listens for clicks on the add button, and if the relevant details are provided adds a new format. Note that formats are responsible for codecing the values into something that can be a) stored in a DOM and b) written to file */
+    private class AddListener
+      implements ActionListener {
+      
+      public void actionPerformed(ActionEvent event) {
+        
+        ignore_event = true; // Prevent format_list excetera propagating events
+        
+        Entry entry = (Entry) feature_combobox.getSelectedItem();
+        Object f = entry.getFeature();
+        String p = "";
+        if (entry.canHavePart()) {
+          p = ((Part)part_combobox.getSelectedItem()).getName();
+        }
+        
+        // Add a new format string of the appropriate type
+        Format format = null;
+        if (view_type.equals(FLAG)) {
+          format = new Format(f, p, enabled_checkbox.isSelected());
+        } else {
+          format = new Format(f, p, editor_textarea.getText());
+          
+        }
+        
+        
+        addFormat(format);
+        existingFormat();
+        
+        // Update list selection
+        format_list.setSelectedValue(format, true);
+        format = null;
+        p = null;
+        f = null;
+        entry = null;
+        ignore_event = false;
+      }
+    }
+    
+    private class EditorListener
+      implements DocumentListener {
+      
+      public void changedUpdate(DocumentEvent e) {
+        
+        update();
+      }
+      
+      public void insertUpdate(DocumentEvent e) {
+        update();
+        updateUndo("insert");
+        
+      }
+      
+      public void removeUpdate(DocumentEvent e) {
+        update();
+        updateUndo("remove");
+        
+      }
+      
+      private void updateUndo(String from){
+        
+        if (!newtext){
+          undo_button.setEnabled(true);
+        }
+        
+        if (editor_textarea.getText().length()!=0 && newtext){
+          newtext = false;
+        }
+      }
+      
+      public void update() {
+	
+        // Determine if replace should be enabled
+        if(!format_list.isSelectionEmpty()) {
+          Format format = (Format)format_list.getSelectedValue();
+          boolean shouldSave = !format.isParamType();
+          
+          if (shouldSave){
+	      // Changed so DocumentButtons statement can be set to empty 
+	      // to get rid of the Detach and NOHIGHLIGHT buttons 
+	      //String format_str = editor_textarea.getText().trim();
+	      //format_str = (format_str.equals(""))? getDefaultFormatString(format.getName()) : format_str;
+            format.setValue(editor_textarea.getText().trim());
+            model.refresh((DOMProxyListEntry)format);
+          }
+          
+        } else {
+          add_button.setEnabled(false);
+          
+        }
+        
+      }
+    }
+    
+    private class EnabledListener
+      implements ActionListener {
+      
+      public void actionPerformed(ActionEvent event) {
+        // If there is a current format selected, and the value of enable_checkbox is now different than to value in it, then enable to replace button.
+        if(!format_list.isSelectionEmpty()) {
+          Format format = (Format)format_list.getSelectedValue();
+          boolean shouldSave = format.isParamType();
+          if (shouldSave){
+            format.setState(enabled_checkbox.isSelected());
+            model.refresh((DOMProxyListEntry)format);
+          }
+        }
+        
+        // Thats it. Add would have been enabled upon feature/part selection depending if no existing format, um, existed.
+      }
+    }
+    
+    private class FeatureListener
+      implements ActionListener {
+      public void actionPerformed(ActionEvent event) {
+        undo_button.setEnabled(false);
+        redo_button.setEnabled(false);
+        default_button.setEnabled(true);
+        newtext = true;
+        
+        if(!ignore_event) {
+          ignore_event = true;
+          Entry entry = (Entry) feature_combobox.getSelectedItem();
+          
+          // Step one: reset part
+          if (entry.canHavePart()) {
+            // update AffectedComponents according to current entry             
+            part_model = entry.getPartModel();
+            part_combobox.setModel(new DefaultComboBoxModel(part_model.toArray()));
+            part_combobox.updateUI();            
+            
+            part_combobox.setEnabled(true);
+            part_combobox.setSelectedIndex(part_combobox.getModel().getSize()-1);                        
+            
+          } else {
+            part_combobox.setEnabled(false);
+            part_combobox.setSelectedIndex(0);
+          }
+          // Step two: the rest
+          
+          String name = entry.toString();
+          // Add is only enabled if there isn't already a format for the choosen feature and part. Create a dummy format and test if itsa already in the model
+          Object f = entry.getFeature();
+          Part part = (Part)part_combobox.getSelectedItem();
+          String pname = part.getName();
+          // You can never add anything to blank-blank
+          if(f.toString().length() == 0 && pname.length() == 0) {
+            add_button.setEnabled(false);
+            remove_button.setEnabled(false);
+          } else {
+            
+            Format format = getFormat(Format.generateName(f, pname));
+            // If there is an existing feature, select it, and use it to determine what controls are visible
+            
+            if(format != null) {
+              ///ystem.err.println("There is an existing format!");
+              format_list.setSelectedValue(format, true);
+              // Now use type to determine what controls are visible, and what have initial values.
+              if(format.isParamType()) {
+                ///ystem.err.println("Flag type");
+                // Flags first.
+                ///election_pane.remove(part_pane);
+                card_layout.show(control_pane, FLAG);
+                view_type = FLAG;
+                // Initial value
+                enabled_checkbox.setSelected(format.getState());
+              } else {
+                ///ystem.err.println("Value type");
+                ///election_pane.add(part_pane);
+                card_layout.show(control_pane, VALUE);
+                view_type = VALUE;
+                // Initial value
+                
+                editor_textarea.setText(format.getValue());
+                editor_textarea.setCaretPosition(0);
+                
+              }
+              existingFormat();
+              control_pane.updateUI();
+            }
+            // Otherwise there is no existing format, so we proceed by checking against the feature name
+            else {
+              ///ystem.err.println("No existing format");
+              format_list.clearSelection();
+              if(Format.isParamType(name)) {
+                ///ystem.err.println("Flag type");
+                // Flags first.
+                ///election_pane.remove(part_pane);
+                card_layout.show(control_pane, FLAG);
+                view_type = FLAG;
+                // Initial value
+                enabled_checkbox.setSelected(false);
+                enabled_checkbox.setEnabled(false);
+              } else {
+                ///ystem.err.println("Value type");
+                ///election_pane.add(part_pane);
+                card_layout.show(control_pane, VALUE);
+                view_type = VALUE;
+                // Initial value
+                
+                String feature_default_format = (String) default_format_map.get(f.toString());
+                if (feature_default_format != null) {
+                  
+                  editor_textarea.setText(feature_default_format);
+                } else {
+                  editor_textarea.setText(part.getDefaultFormat());
+                }
+                editor_textarea.setCaretPosition(0);
+                
+              }
+              newFormat();
+            }
+            format = null;
+            name = null;
+          }
+          part = null;
+          pname = null;
+          f = null;
+          name = null;
+          entry = null;
+          ignore_event = false;
+        }
+        undo.discardAllEdits();
+      }
+    }
+    
+    private class FormatListListener
+      implements ListSelectionListener {
+      public void valueChanged(ListSelectionEvent event) {
+        undo_button.setEnabled(false);
+        redo_button.setEnabled(false);
+        default_button.setEnabled(true);
+        newtext = true;
+        
+        if(!ignore_event && !event.getValueIsAdjusting()) {
+          
+          if(!format_list.isSelectionEmpty()) {
+            existingFormat();
+            ignore_event = true;
+            Format format = (Format)format_list.getSelectedValue();
+            // Try to match the target, remembering the entries within are Entry's
+            Entry an_entry = new Entry(format.getFeature());            
+            feature_combobox.setSelectedItem(an_entry);            
+            // If we didn't match anything, add it and select it again
+            Entry result_entry = (Entry) feature_combobox.getSelectedItem();
+            if(!an_entry.equals(result_entry)) {
+              feature_combobox.insertItemAt(an_entry, feature_combobox.getItemCount());
+              feature_combobox.setSelectedItem(an_entry);
+            }
+            
+            // Now use type to determine what controls are visible, and what have initial values.
+            ///ystem.err.println("Determine the correct type.");
+            if(format.isParamType()) {
+              ///ystem.err.println("Flag type - remove part");
+              // Flags first.
+              ///SwingUtilities.invokeLater(new GUIChangeTask(/election_pane, part_pane, blank_pane, false));
+              card_layout.show(control_pane, FLAG);
+              view_type = FLAG;
+              // Initial value
+              enabled_checkbox.setSelected(format.getState());
+            } else {
+              ///ystem.err.println("Value type - show part");
+              ///SwingUtilities.invokeLater(new GUIChangeTask(/election_pane, part_pane, blank_pane, true));
+              card_layout.show(control_pane, VALUE);
+              view_type = VALUE;
+              // Initial values
+              // do we have a part?
+              if (format.canHavePart()) {
+                part_combobox.setEnabled(true);
+                
+                // Should update the part list and re-render the part ComboBox here
+                part_model = result_entry.getPartModel();
+                part_combobox.setModel(new DefaultComboBoxModel(part_model.toArray()));
+                part_combobox.updateUI();      
+                
+                // Try to match the part.
+                String part_entry = format.getPart();
+                // Set Selected Item doesn't work so I'll do this manually
+                boolean found = false;
+                for(int i=0; i < part_combobox.getItemCount(); i++) {
+                  Part a_part = (Part) part_combobox.getItemAt(i);
+                  if(a_part.equals(part_entry)) {
+                    part_combobox.setSelectedItem(a_part);
+                    found = true;
+                  }
+                  a_part = null;
+                }
+                // If we didn't match anything, add it and select it again
+                if(!found) {
+                  Part a_part = new Part(part_entry, "");
+                  part_combobox.insertItemAt(a_part, part_combobox.getItemCount());
+                  part_combobox.setSelectedItem(a_part);
+                  
+                }
+              } else {
+                part_combobox.setEnabled(false);
+                part_combobox.setSelectedIndex(0);
+              }
+              editor_textarea.setText(format.getValue());
+              editor_textarea.setCaretPosition(0);
+              
+            }
+            
+            //control_pane.updateUI();
+            ignore_event = false;
+          }
+          
+        }
+        undo.discardAllEdits();
+      }
+      
+    }
+    
+    private void newFormat(){
+      editor_textarea.setEditable(false);
+      editor_textarea.setBackground(Color.lightGray);
+      editor_textarea.setToolTipText(Dictionary.get("CDM.FormatManager.Editor_Disabled_Tooltip"));
+      
+      enabled_checkbox.setEnabled(false);
+      undo_button.setEnabled(false);
+      redo_button.setEnabled(false);
+      variable_combobox.setEnabled(false);
+      add_button.setEnabled(true);
+      remove_button.setEnabled(false);
+      default_button.setEnabled(false);
+      
+    }
+    
+    private void existingFormat(){
+      editor_textarea.setEditable(true);
+      editor_textarea.setBackground(Color.white);
+      editor_textarea.setToolTipText(Dictionary.get("CDM.FormatManager.Editor_Tooltip"));
+      enabled_checkbox.setEnabled(true);
+      variable_combobox.setEnabled(true);
+      add_button.setEnabled(false);
+      remove_button.setEnabled(true);
+      default_button.setEnabled(true);
+      
+    }
+    
+    private class VariableListener
+      implements ActionListener {
+      public void actionPerformed(ActionEvent event) {
+        int selected_index = variable_combobox.getSelectedIndex();
+        if (selected_index == 0) return;
+        String selected_value = (String)variable_combobox.getSelectedItem();
+        editor_textarea.insert(selected_value, editor_textarea.getCaretPosition());
+        undo_button.setEnabled(true);
+        variable_combobox.setSelectedIndex(0);
+      }
+      
+    }
+    
+    private class PartListener
+      implements ActionListener {
+      public void actionPerformed(ActionEvent event) {
+        undo_button.setEnabled(false);
+        redo_button.setEnabled(false);
+        default_button.setEnabled(true);
+        newtext = true;
+        if(!ignore_event) {
+          // Add is only enabled if there isn't already a format for the choosen feature and part. Create a dummy format and test if its already in the model
+          Entry entry = (Entry) feature_combobox.getSelectedItem();
+          Object f = entry.getFeature();
+          Part part = (Part) part_combobox.getSelectedItem();
+          String pname = part.getName();
+          // You can never add anything to blank-blank
+          if(f.toString().length() == 0 && pname.length() == 0) {
+            add_button.setEnabled(false);
+            remove_button.setEnabled(false);
+          } else {
+            String name = Format.generateName(f, pname);
+            Format format = getFormat(name);
+            // If there is an existing feature, select it.
+            if(format != null) {
+              format_list.setSelectedValue(format, true);
+              // Now use type to determine what controls are visible, and what have initial values.
+              if(format.isParamType()) {
+                // Flags first.
+                ///election_pane.remove(part_pane);
+                card_layout.show(control_pane, FLAG);
+                view_type = FLAG;
+                // Initial value
+                enabled_checkbox.setSelected(format.getState());
+              } else {
+                ///election_pane.add(part_pane);
+                card_layout.show(control_pane, VALUE);
+                view_type = VALUE;
+                // Initial value
+                
+                editor_textarea.setText(format.getValue());
+                editor_textarea.setCaretPosition(0);
+                
+              }
+              control_pane.updateUI();
+              existingFormat();
+            } else {
+              format_list.clearSelection();
+              if(Format.isParamType(name)) {
+                // Flags first.
+                ///election_pane.remove(part_pane);
+                card_layout.show(control_pane, FLAG);
+                view_type = FLAG;
+                // Initial value
+                enabled_checkbox.setSelected(false);
+                enabled_checkbox.setEnabled(false);
+              } else {
+                ///election_pane.add(part_pane);
+                card_layout.show(control_pane, VALUE);
+                view_type = VALUE;
+                // Initial value
+                
+                editor_textarea.setText(part.getDefaultFormat());
+                editor_textarea.setCaretPosition(0);
+                
+              }
+              newFormat();
+            }
+            format = null;
+            name = null;
+          }
+          
+          pname = null;
+          part = null;
+          f = null;
+          entry = null;
+        }
+        undo.discardAllEdits();
+      }
+    }
+    
+    private class RemoveListener
+      implements ActionListener {
+      
+      public void actionPerformed(ActionEvent event) {
+        if (!format_list.isSelectionEmpty()) {
+          // Remove the current format
+          removeFormat((Format)format_list.getSelectedValue());
+          
+          // Change buttons
+          add_button.setEnabled(true);
+          newFormat();
+        }
+      }
+    }
+    
+    
+    
+    private class DefaultListener
+      implements ActionListener {
+      
+      public void actionPerformed(ActionEvent event) {
+	  
+	  // ensure that the user didn't accidentally press the Reset to Default button
+	  int user_choice = JOptionPane.showConfirmDialog(Gatherer.g_man, Dictionary.get("CDM.FormatManager.Default_Warning"), Dictionary.get("General.Warning"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
+	  if(user_choice == JOptionPane.CANCEL_OPTION) {
+	      return;
+	  }
+	  
+        newtext = false;
+        if(!ignore_event) {
+          Entry entry = (Entry) feature_combobox.getSelectedItem();
+          Object f = entry.getFeature();
+          String name ="";
+          String pname="";
+          Part part = null;
+          
+          if (entry.canHavePart()){
+            part = (Part) part_combobox.getSelectedItem();
+            pname = part.getName();
+            name = Format.generateName(f, pname);
+          } else{
+            name = entry.toString();
+          }
+          
+          Format format = getFormat(name);
+          // If there is an existing feature, select it.
+          if(format != null) {
+            remove_button.setEnabled(true);
+          } else {
+            
+            add_button.setEnabled(true);
+          }//endif (format !=null)
+          
+          if(Format.isParamType(name)) {
+            // Flags first.
+            card_layout.show(control_pane, FLAG);
+            view_type = FLAG;
+            // Initial value
+            enabled_checkbox.setSelected(false);
+          } else {
+            card_layout.show(control_pane, VALUE);
+            view_type = VALUE;
+            // Initial value
+            if (pname !=null && pname.length()!=0 ){
+              editor_textarea.setText((String) default_format_map.get(pname));
+              editor_textarea.setCaretPosition(0);
+            } else{
+              editor_textarea.setText((String) default_format_map.get(name));
+              editor_textarea.setCaretPosition(0);
+            }
+          }
+          
+	}
+      }
+      
+    }
+    
+    private class UndoListener
+      implements ActionListener {
+      
+      public void actionPerformed(ActionEvent event) {
+        try {
+          if (undo.canUndo()) {
+            int pos = editor_textarea.getCaretPosition();
+            redo_button.setEnabled(true);
+            undo.undo();
+            if (pos > 0)
+              editor_textarea.setCaretPosition(pos-1);
+            else
+              editor_textarea.setCaretPosition(pos);
+          }
+          if (!undo.canUndo()){
+            undo_button.setEnabled(false);
+          } else{
+            undo_button.setEnabled(true);
+          }
+          
+        } catch (Exception e) {
+          
+        }
+      }
+    }
+    
+    private class RedoListener
+      implements ActionListener {
+      public void actionPerformed(ActionEvent evt) {
+        try {
+          if (undo.canRedo()) {
+            int pos = editor_textarea.getCaretPosition();
+            undo.redo();
+            editor_textarea.setCaretPosition(pos);
+          }
+          if (!undo.canRedo()){
+            redo_button.setEnabled(false);
+          } else{
+            redo_button.setEnabled(true);
+          }
+          
+        } catch (Exception e) {}
+      }
+    }
+    
+  }
+  
+  /** This object provides a wrapping around an entry in the format target control, which is tranparent as to whether it is backed by a String or a Classifier. */
+  private class Entry
+    implements Comparable {
+    private Classifier classifier = null;
+    private String text = null;
+    
+    public Entry(Object object) {
+      if(object instanceof Classifier) {
+        classifier = (Classifier)object;
+      } else if(object instanceof String) {
+        text = (String)object;
+      } else {
+        text = "";
+      }
+    }
+    
+    public Entry(String text) {
+      this.text = text;
+    }
+    
+    public boolean canHavePart() {
+      if (classifier !=null) return true;
+      return Format.canHavePart(text);
+    }
+    
+    public int compareTo(Object object) {
+      if(object == null) {
+        return 1;
+      }
+      if(toString() == null) {
+        return -1;
+      } else {
+        String object_str = object.toString();
+        if(object_str == null) {
+          return 1;
+        }
+        return toString().compareTo(object_str);
+      }
+    }
+    
+    public boolean equals(Object object) {
+      if(compareTo(object) == 0) {
+        return true;
+      }
+      return false;
+    }
+    
+    public Classifier getClassifier() {
+      return classifier;
+    }
+    
+    public Object getFeature() {
+      if(classifier != null) {
+        return classifier;
+      }
+      
+      if (text.startsWith("<html>")){
+        return  "";
+      }
+      return text;
+    }
+    
+    public ArrayList getPartModel(){
+      DebugStream.println("getPartModel(): get appopriate affected components of this classifier");
+      String classifier_name = classifier == null ? "" : classifier.toString();
+      if(!classifier_name.equals("")){
+        classifier_name = classifier_name.substring(0, classifier_name.indexOf("-")).trim();        
+      }      
+      ArrayList part_model = new ArrayList();
+      // DateList only has DateList component
+      if(classifier_name.equals("DateList")){
+        part_model.add(new Part(DATELIST, DATELIST_DEFAULT_FORMAT));
+      } 
+      // Other Format Entries, display the default Affected Components
+      else if(classifier_name.equals("")){      
+        part_model = buildPartModel();      
+      }
+      // Other Classifiers have HList and VList as Affected Component
+      else {
+        part_model.add(new Part(HLIST, HLIST_DEFAULT_FORMAT));
+        part_model.add(new Part(VLIST, VLIST_DEFAULT_FORMAT));
+      }
+      return part_model;
+    }
+    
+    public String toString() {
+      if(classifier != null) {
+        // Return the classifier - with its CL index shown
+        return classifier.getPositionString() + StaticStrings.COLON_CHARACTER + StaticStrings.SPACE_CHARACTER + classifier.toString();
+      }
+      if (text.equals("")) {
+        return "<html><body><i>"+Dictionary.get("CDM.FormatManager.AllFeatures")+"</i></body></html>";
+      }
+      return text;
+    }
+  }
+  
+    /*
+      private class GUIChangeTask
+      implements Runnable {
+      private boolean to_add;
+      private JPanel child;
+      private JPanel parent;
+      private JPanel replacement;
+     
+      public GUIChangeTask(JPanel parent, JPanel child, JPanel replacement, boolean to_add) {
+      this.child = child;
+      this.parent = parent;
+      this.replacement = replacement;
+      this.to_add = to_add;
+      }
+     
+      public void run() {
+      if(to_add) {
+      parent.remove(replacement);
+      parent.add(child);
+      parent.updateUI();
+      }
+      else {
+      parent.remove(child);
+      parent.add(replacement);
+      parent.updateUI();
+      }
+      }
+      }
+     */
+  
+  /** This class encapsulates all of the information associated with a certain component part of a feature within a html page returned from the receptioninst. */
+  private class Part
+    implements Comparable {
+    /** The default format string for this part */
+    private String default_format = null;
+    /** The name of this part */
+    private String name = null;
+    /** Constructor - must be provided with all of the details of a part as there are no other setter methods.
+     * @param name the name of this part
+     * @param default_format the default format string for this part
+     */
+    public Part(String name, String default_format) {
+      this.default_format = default_format;
+      this.name = name;
+    }
+    /** Compare this part to another object in terms of ordering
+     * @param obj the other Object
+     * @return <0 if the object is before, 0 if equal to, and >0 if the object is after this part
+     */
+    public int compareTo(Object obj) {
+      if (obj instanceof Part) {
+        return name.compareTo(((Part)obj).getName());
+      }
+      return name.compareTo(obj.toString());
+    }
+    
+    /** Determine if the part is equivelent to some other object
+     * @param obj the other Object
+     * @return true is the two objects are equal
+     */
+    public boolean equals(Object obj) {
+      if (obj instanceof Part) {
+        return name.equals(((Part)obj).getName());
+      }
+      return name.equals(obj.toString());
+    }
+    
+    /** Retrieve the default format string for this part
+     * @return the default format String
+     */
+    public String getDefaultFormat() {
+      // Retrieve the format for the super format - either VList or HList
+      Format default_format_object = getFormat(name);
+      if(default_format_object != null) {
+        return default_format_object.getValue();
+      } else {
+        return this.default_format;
+      }
+    }
+    /** Retrieve the name of this part
+     * @return the name as a String
+     */
+    public String getName() {
+      return name;
+    }
+    /** Produce a string representation of this part, which in this case is simply the name again
+     * @return the name as a String
+     */
+    public String toString() {
+      if (name.equals("")) {
+        return "<html><body><i>"+Dictionary.get("CDM.FormatManager.AllParts")+"</i></body></html>";
+      }
+      return name;
+    }
+  }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/GeneralManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/GeneralManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/GeneralManager.java	(revision 31635)
@@ -0,0 +1,451 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.regex.*;
+import javax.swing.*;
+import javax.swing.filechooser.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.EmailField;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.GUIUtils;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.Gatherer;
+
+/** This class provides a graphical interface for editing general metadata for the collection, such as name, description, icons etc. 
+*/
+public class GeneralManager {
+
+    /** The controls used to modify the general options. */
+    private Control controls;
+    /** Constructor. */
+    public GeneralManager() {
+    }
+
+    /** Destructor. */
+    public void destroy() {
+	if (controls != null) {
+	    controls.destroy();
+	    controls = null;
+	}
+    }
+
+    public void loseFocus() {
+    }
+
+    public void gainFocus() {
+
+    }
+    /** This class is resposible for generating the controls for the editing of general options.
+     * @return the Control for editing the general options
+     */
+    public Control getControls() {
+	if (controls == null) {
+	    controls = new GeneralControl();
+	}
+	return controls;
+    }
+    
+
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+
+    }
+    
+    /** This class represents the visual component of the general options stored in the CollectionDesignManager. */
+    private class GeneralControl
+	extends JPanel
+	implements Control {
+
+        private boolean ready = false;
+	private CollectionMeta collection_extra_collectionmeta;
+	private CollectionMeta collection_name_collectionmeta;
+	private CollectionMeta creator_collectionmeta;
+	private CollectionMeta icon_collection_collectionmeta;
+	private CollectionMeta icon_collection_small_collectionmeta;
+	private CollectionMeta maintainer_collectionmeta;
+	private CollectionMeta public_collectionmeta;
+	/** The creators email. */
+	private EmailField creator_emailfield;
+	/** The maintainers email. */
+	private EmailField maintainer_emailfield;
+	/** Button to browse for the collection about page icon. */
+	private JButton browse_about_icon_button;
+	/** Button to browse for the collection home page icon. */
+	private JButton browse_home_icon_button;
+	/** The checkbox controlling public access to the collection. */
+	private JCheckBox public_checkbox;
+	private JLabel creator_label;
+	private JLabel description_label;
+	private JLabel icon_label;
+	private JLabel maintainer_label;
+	private JLabel name_label;
+	private JLabel small_icon_label;
+	/** The text field used to edit the file name of the collections icon. */
+	private JTextField icon_textfield;
+	/** The text field used to edit the collections title. */
+	private JTextField name_textfield;
+	/** The text field used to edit the file name of the collections small icon. */
+	private JTextField small_icon_textfield;
+	/** A text area used to modify the collection description. */
+	private JTextArea description_textarea;
+	/** Constructor. */
+	public GeneralControl() {
+	    super();
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    // Retrieve some of the model elements, those we know aren't language dependant
+	    public_collectionmeta = new CollectionMeta(CollectionDesignManager.collect_config.getPublic());
+
+	    // Creation
+	    JPanel header_panel = new DesignPaneHeader("CDM.GUI.General", "generalsettings");
+            
+	    JPanel all_details_panel = new JPanel();
+	    JPanel details_panel = new JPanel();
+	    JPanel fields_panel = new JPanel();
+
+            all_details_panel.setComponentOrientation(Dictionary.getOrientation());
+            details_panel.setComponentOrientation(Dictionary.getOrientation());
+            fields_panel.setComponentOrientation(Dictionary.getOrientation());
+                        
+	    creator_label = new JLabel(Dictionary.get("CDM.General.Email.Creator"));
+	    creator_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    creator_emailfield = new EmailField(Configuration.getColor("coloring.error_background", false));
+	    creator_emailfield.setToolTipText(Dictionary.get("CDM.General.Email.Creator_Tooltip"));
+	    
+	    maintainer_label = new JLabel(Dictionary.get("CDM.General.Email.Maintainer"));
+	    maintainer_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    maintainer_emailfield = new EmailField(Configuration.getColor("coloring.error_background", false));
+	    maintainer_emailfield.setToolTipText(Dictionary.get("CDM.General.Email.Maintainer_Tooltip"));
+	    
+	    name_label = new JLabel(Dictionary.get("CDM.General.Collection_Name"));
+            name_label.setComponentOrientation(Dictionary.getOrientation());
+	    name_textfield = new JTextField();
+            name_textfield.setComponentOrientation(Dictionary.getOrientation());
+	    name_textfield.setToolTipText(Dictionary.get("CDM.General.Collection_Name_Tooltip"));
+	    
+	    JLabel short_name_label = new JLabel(Dictionary.get("NewCollectionPrompt.Collection_Name"));
+	    short_name_label.setComponentOrientation(Dictionary.getOrientation());
+            JTextField short_name_textfield = new JTextField(CollectionManager.getLoadedCollectionName());
+            short_name_textfield.setComponentOrientation(Dictionary.getOrientation());
+	    short_name_textfield.setEditable(false);
+	    short_name_textfield.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    JPanel icon_panel = new JPanel();
+            icon_panel.setComponentOrientation(Dictionary.getOrientation());
+	    icon_label = new JLabel(Dictionary.get("CDM.General.Icon_Collection"));
+            icon_label.setComponentOrientation(Dictionary.getOrientation());
+	    icon_textfield = new JTextField();
+            icon_textfield.setComponentOrientation(Dictionary.getOrientation());
+	    icon_textfield.setToolTipText(Dictionary.get("CDM.General.Icon_Collection_Tooltip")); 
+	    browse_about_icon_button = new GLIButton(Dictionary.get("General.Browse"));
+	    JPanel small_icon_panel = new JPanel();
+            small_icon_panel.setComponentOrientation(Dictionary.getOrientation());
+	    small_icon_label = new JLabel(Dictionary.get("CDM.General.Icon_Collection_Small"));
+            small_icon_label.setComponentOrientation(Dictionary.getOrientation());
+	    small_icon_textfield = new JTextField();
+            small_icon_textfield.setComponentOrientation(Dictionary.getOrientation());
+	    small_icon_textfield.setToolTipText(Dictionary.get("CDM.General.Icon_Collection_Small_Tooltip"));
+	    browse_home_icon_button = new GLIButton(Dictionary.get("General.Browse"));
+
+	    // public
+	    JPanel box_panel = new JPanel();
+            box_panel.setComponentOrientation(Dictionary.getOrientation());
+	    public_checkbox = new JCheckBox(Dictionary.get("CDM.General.Access"), public_collectionmeta.getValue(CollectionMeta.TEXT).equals(StaticStrings.TRUE_STR));
+	    public_checkbox.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel description_panel = new JPanel();
+            description_panel.setComponentOrientation(Dictionary.getOrientation());
+	    description_label = new JLabel(Dictionary.get("CDM.General.Collection_Extra"));
+	    description_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    description_textarea = new JTextArea();
+            description_textarea.setComponentOrientation(Dictionary.getOrientation());
+	    description_textarea.setBackground(Configuration.getColor("coloring.editable_background", false));
+	    description_textarea.setToolTipText(Dictionary.get("CDM.General.Collection_Extra_Tooltip"));
+	    
+	    // Connection
+	    BrowseListener browse_listener = new BrowseListener(StaticStrings.IMAGES_PATH_RELATIVE_TO_GSDL_PREFIX);
+	    browse_about_icon_button.addActionListener(browse_listener);
+	    browse_home_icon_button.addActionListener(browse_listener);
+	    browse_listener = null;
+	    public_checkbox.addActionListener(CollectionDesignManager.change_listener);
+	    creator_emailfield.getDocument().addDocumentListener(CollectionDesignManager.change_listener);
+	    description_textarea.getDocument().addDocumentListener(CollectionDesignManager.change_listener);
+	    icon_textfield.getDocument().addDocumentListener(CollectionDesignManager.change_listener);
+	    maintainer_emailfield.getDocument().addDocumentListener(CollectionDesignManager.change_listener);
+	    name_textfield.getDocument().addDocumentListener(CollectionDesignManager.change_listener);
+	    name_textfield.getDocument().addDocumentListener(new CollectionTitleUpdater());
+	    small_icon_textfield.getDocument().addDocumentListener(CollectionDesignManager.change_listener);
+
+	    // Layout
+	    fields_panel.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    fields_panel.setLayout(new BorderLayout(5,2));
+	    
+	    JPanel fields_label_panel = new JPanel();
+            fields_label_panel.setComponentOrientation(Dictionary.getOrientation());
+	    fields_label_panel.setLayout(new GridLayout(6,1));
+
+	    JPanel fields_box_panel = new JPanel();
+            fields_box_panel.setComponentOrientation(Dictionary.getOrientation());            
+	    fields_box_panel.setLayout(new GridLayout(6,1));
+    
+	    // creator
+	    fields_label_panel.add(creator_label);
+	    fields_box_panel.add(creator_emailfield);
+
+	    // maintainer
+	    fields_label_panel.add(maintainer_label);
+	    fields_box_panel.add(maintainer_emailfield);
+
+	    // title
+	    fields_label_panel.add(name_label);
+	    fields_box_panel.add(name_textfield);
+
+	    // collection short name
+	    fields_label_panel.add(short_name_label);
+	    fields_box_panel.add(short_name_textfield);
+
+	    // icon
+	    fields_label_panel.add(icon_label);
+	    fields_box_panel.add(icon_panel);
+
+	    // small icon
+	    fields_label_panel.add(small_icon_label);
+	    fields_box_panel.add(small_icon_panel);
+
+	    fields_panel.add(fields_label_panel, BorderLayout.LINE_START);
+	    fields_panel.add(fields_box_panel, BorderLayout.CENTER);
+
+	    icon_panel.setLayout(new BorderLayout());
+	    icon_panel.add(icon_textfield, BorderLayout.CENTER);
+	    icon_panel.add(browse_about_icon_button, BorderLayout.LINE_END);
+
+	    small_icon_panel.setLayout(new BorderLayout());
+	    small_icon_panel.add(small_icon_textfield, BorderLayout.CENTER);
+	    small_icon_panel.add(browse_home_icon_button, BorderLayout.LINE_END);
+
+	    box_panel.setLayout(new GridLayout(1,1,5,2));
+	    box_panel.add(public_checkbox);
+
+	    description_panel.setLayout(new BorderLayout());
+	    description_panel.add(description_label, BorderLayout.NORTH);
+	    description_panel.add(new JScrollPane(description_textarea), BorderLayout.CENTER);
+	    
+	    details_panel.setLayout(new BorderLayout());
+	    details_panel.add(fields_panel, BorderLayout.NORTH);
+	    details_panel.add(box_panel, BorderLayout.CENTER);
+
+	    all_details_panel.setLayout(new BorderLayout());
+	    all_details_panel.add(details_panel, BorderLayout.NORTH);
+	    all_details_panel.add(description_panel, BorderLayout.CENTER);
+
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(header_panel, BorderLayout.NORTH);
+	    add(all_details_panel, BorderLayout.CENTER);
+	}
+
+	/** Destructor. */
+	public void destroy() {
+	}
+
+	/** Called to refresh the components. */
+	public void gainFocus() {
+             // Retrieve all of the elements that are dependant on default language.
+	    collection_extra_collectionmeta = CollectionDesignManager.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_COLLECTIONEXTRA_STR);
+	    collection_name_collectionmeta = CollectionDesignManager.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_COLLECTIONNAME_STR);
+	    
+	    creator_collectionmeta = new CollectionMeta(CollectionDesignManager.collect_config.getCreator());
+	    icon_collection_collectionmeta = CollectionDesignManager.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_ICONCOLLECTION_STR);
+	    icon_collection_small_collectionmeta = CollectionDesignManager.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_ICONCOLLECTIONSMALL_STR);
+	    maintainer_collectionmeta = new CollectionMeta(CollectionDesignManager.collect_config.getMaintainer());
+	    // Make sure the components are up to date
+	    creator_emailfield.setText(creator_collectionmeta.getValue(CollectionMeta.TEXT));
+	    creator_emailfield.setCaretPosition(0);
+	    description_textarea.setText(collection_extra_collectionmeta.getValue(CollectionMeta.TEXT));
+	    description_textarea.setCaretPosition(0);
+	    icon_textfield.setText(icon_collection_collectionmeta.getValue(CollectionMeta.TEXT));
+	    icon_textfield.setCaretPosition(0);
+	    maintainer_emailfield.setText(maintainer_collectionmeta.getValue(CollectionMeta.TEXT));
+	    maintainer_emailfield.setCaretPosition(0);
+	    name_textfield.setText(collection_name_collectionmeta.getValue(CollectionMeta.TEXT));
+	    name_textfield.setCaretPosition(0);
+	    small_icon_textfield.setText(icon_collection_small_collectionmeta.getValue(CollectionMeta.TEXT));
+	    small_icon_textfield.setCaretPosition(0);
+	    public_checkbox.setSelected(public_collectionmeta.getValue(CollectionMeta.TEXT).equals(StaticStrings.TRUE_STR));
+            ready = true;
+	}
+
+	/** Called to store the current value of the components. */
+	public void loseFocus() {
+	    // String values. Have to test if this component has actually ever recieved focus anyway.
+	    
+            if (!ready) return;
+
+	    public_collectionmeta.setValue((public_checkbox.isSelected() ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	    String creator_email_str = creator_emailfield.getText();
+	    creator_collectionmeta.setValue(creator_email_str);
+	    collection_extra_collectionmeta.setValue(description_textarea.getText());
+	    icon_collection_collectionmeta.setValue(icon_textfield.getText());
+	    maintainer_collectionmeta.setValue(maintainer_emailfield.getText());
+	    icon_collection_small_collectionmeta.setValue(small_icon_textfield.getText());
+	    collection_name_collectionmeta.setValue(name_textfield.getText());
+	 
+	
+    }
+
+	/** Listens for a click on either browse button, and when detected opens a file browser window initially pointed at the images folder of the collection. Once a user has selected a path, does it's best to construct the best address to the resource, such as _httpcollection_/images/about.gif */
+	private class BrowseListener
+	    implements ActionListener {
+
+	    private String prefix;
+
+	    public BrowseListener(String prefix_raw) {
+		this.prefix = prefix_raw.replaceAll(StaticStrings.COLNAME_PATTERN, CollectionManager.getLoadedCollectionName());
+	    }
+
+	    public void actionPerformed(ActionEvent event) {
+		// Open an almost standard file browser to the images folder of the current collection
+		File images_folder = new File(CollectionManager.getLoadedCollectionImagesDirectoryPath());
+		// If images isn't already there, create it
+		if(!images_folder.exists()) {
+		    images_folder.mkdirs();
+		}
+		JFileChooser file_chooser = new JFileChooser(images_folder);
+		file_chooser.setAcceptAllFileFilterUsed(false);
+		file_chooser.setDialogTitle(Dictionary.get("CDM.General.Browser_Title"));
+		file_chooser.setFileFilter(new ImageFilter());
+		file_chooser.setSize(400,300);
+		GUIUtils.disableRename(file_chooser);
+		int value = file_chooser.showOpenDialog(Gatherer.g_man);
+		// If the user hasn't cancelled, retrieve the file path selected
+		if(value == JFileChooser.APPROVE_OPTION) {
+		    // If the file isn't in the images folder, then ask the user if they want to copy it there
+		    File file = file_chooser.getSelectedFile();
+		    if (!file.getParentFile().equals(images_folder)) {
+			// Copy the file
+			try {
+			    File collection_image_file = new File(images_folder, file.getName());
+			    Gatherer.f_man.getQueue().copyFile(file, collection_image_file, true);
+
+			    // If we're using a remote Greenstone server, upload the image
+			    if (Gatherer.isGsdlRemote) {
+				Gatherer.remoteGreenstoneServer.uploadCollectionFile(
+                                   CollectionManager.getLoadedCollectionName(), collection_image_file);
+			    }
+			}
+			catch(Exception exception) {
+			    DebugStream.printStackTrace(exception);
+			    // Show warning
+			    String[] args = new String[] {file.getAbsolutePath(), images_folder.getAbsolutePath()};
+			    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.General.Image_Copy_Failed", args), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+			    // Nothing else we can do.
+			    return;
+			}
+		    }
+
+		    // Create the path starting _httpcollection_/images/<filename>
+		    String path = prefix + file.getName();
+		    if (Gatherer.GS3){
+			path = file.getName();
+		    }
+		    if(event.getSource() == browse_about_icon_button) {
+			icon_textfield.setText(path);
+                        icon_collection_collectionmeta.setValue(icon_textfield.getText());
+		    }
+		    else {
+			small_icon_textfield.setText(path);
+                        icon_collection_small_collectionmeta.setValue(small_icon_textfield.getText());
+		    }
+		    path = null;
+		}
+	    }
+
+	    /** ImageFilter.java is a 1.4 example used by FileChooserDemo2.java. */
+	    private class ImageFilter
+		extends javax.swing.filechooser.FileFilter {
+
+		private Pattern pattern = null;
+
+		public ImageFilter() {
+		    pattern = Pattern.compile(".*\\.(gif|png|jpe?g)");
+		}
+
+		// Accept all directories and all .col files
+		public boolean accept(File f) {
+		    String filename = f.getName().toLowerCase();
+		    Matcher matcher = pattern.matcher(filename);
+		    return f.isDirectory() || matcher.matches();
+		}
+
+		// The description of this filter
+		public String getDescription() {
+		    return Dictionary.get("CDM.General.Image_Filter");
+		}
+	    }
+	}
+
+	private class CollectionTitleUpdater
+	    implements DocumentListener {
+	    /** Gives notification that an attribute or set of attributes changed. */
+	    public void changedUpdate(DocumentEvent e) {
+		setTitle();
+	    }
+	    /** Gives notification that there was an insert into the document. */
+	    public void insertUpdate(DocumentEvent e) {
+		setTitle();
+	    }
+	    /** Gives notification that a portion of the document has been removed. */
+	    public void removeUpdate(DocumentEvent e) {
+		setTitle();
+	    }
+	    
+	    private void setTitle() {
+		// Set the title
+		String collection_title = name_textfield.getText();
+		String collection_name = CollectionManager.getLoadedCollectionName(true);
+		Gatherer.g_man.setTitle(collection_title, collection_name);
+                collection_name_collectionmeta.setValue(collection_title);
+		collection_title = null;
+		collection_name = null;
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Index.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Index.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Index.java	(revision 31635)
@@ -0,0 +1,309 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.util.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+/** This class encapsulates a single indexing pair.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3d
+ */
+public class Index
+    implements Comparable, DOMProxyListEntry {
+    /** An values of items in the index level enumeration. */
+    static protected final String LEVEL[] = {StaticStrings.DOCUMENT_STR,  StaticStrings.SECTION_STR, StaticStrings.PARAGRAPH_STR};
+
+    private ArrayList sources = null;
+    /** The level of this index (if old sckool MG). */
+    private int level = -1;
+    /** The element this index is based upon. */
+    private Element element = null;
+    /** The unique, if cryptic, identifier of an index. */
+    private String id = null;
+
+    /** Default constructor, which should only be used during DOMProxyListModel creation. */
+    public Index() {
+    }
+
+    /** Constructor. */
+    public Index(Element element) {
+	this.element = element;
+    }
+
+    /** Constructor for a newly assigned index. */
+    public Index(ArrayList sources) {
+	this.sources = sources;
+	// Create a new element
+	this.element = CollectionConfiguration.createElement(StaticStrings.INDEX_ELEMENT);
+	// For each source add a content element
+	int size = sources.size();
+	for(int i = 0; i < size; i++) {
+	    Element content_element = CollectionConfiguration.createElement(StaticStrings.CONTENT_ELEMENT);
+	    Object source_object = sources.get(i);
+	    if (source_object instanceof MetadataElement) {
+		// System.err.println("Constructing new Index with MetadataElement source...");
+		content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, ((MetadataElement) source_object).getFullName());
+	    }
+	    else {
+		// System.err.println("Constructing new Index with String source...");
+		content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, source_object.toString());
+	    }
+	    element.appendChild(content_element);
+	    content_element = null;
+	}
+    }
+
+    /** Constructor for a newly assigned index with specified level (old skool). */
+    public Index(int level, ArrayList sources) {
+	this(sources);
+	this.level = level;
+	element.setAttribute(StaticStrings.LEVEL_ATTRIBUTE, LEVEL[level]);
+    }
+
+    public Index(String level, ArrayList sources) {
+	this(sources);
+	for(int i = 0; i < LEVEL.length; i++) {
+	    if(LEVEL[i].equalsIgnoreCase(level)) {
+		this.level = i;
+	    }
+	}
+	element.setAttribute(StaticStrings.LEVEL_ATTRIBUTE, LEVEL[this.level]);
+    }
+
+    /** Method to compare two indexes.
+     * @param object The other index as an <strong>Object</strong>.
+     * @return An <i>int</i> which indicates how the indexes compare.
+     * @see java.lang.String
+     */
+    public int compareTo(Object object) {
+	if(object == null) {
+	    return -1;
+	}
+	String id = getID();
+	return id.compareTo(((Index)object).getID());
+    }
+    
+    public DOMProxyListEntry create(Element element) {
+	return new Index(element);
+    }
+
+    /** Method to test for the equality of two indexes.
+     * @param object The other index as an <strong>Object</strong>.
+     * @return A <i>boolean</i> which is <i>true</i> if the two indexes are equal, <i>false</i> otherwise.
+     */
+    public boolean equals(Object object) {
+	return (compareTo(object) == 0);
+    }
+    
+    public Element getElement() {
+	return element;
+    }
+    
+    /** Method to get the value of level.
+     * @return the level as a int
+     */
+    public int getLevel() {
+	if(level == -1 && element != null) {
+	    String level_str = element.getAttribute(StaticStrings.LEVEL_ATTRIBUTE);
+	    for(int i = 0; level == -1 && i < LEVEL.length; i++) {
+		if(level_str.equals(LEVEL[i])) {
+		    level = i;
+		}
+	    }
+	    level_str = null;
+	}
+	return level;
+    }
+
+    public String getID() {
+	if(element == null) {
+	    id="";
+	}
+	else if(id == null) {
+	    StringBuffer id_buffer = new StringBuffer();
+	    // Write level information, if any.
+	    int level = getLevel();
+	    if(0 <= level && level < 3) {
+		id_buffer.append(LEVEL[level]);
+		id_buffer.append(StaticStrings.COLON_CHARACTER);
+	    }
+	    // Write data information. Retrieve each of the content sources and add them in a comma separated list.
+	    ArrayList sources = getSources();
+	    int sources_size = sources.size();
+	    for(int i = 0; i < sources_size; i++) {
+		Object source_object = sources.get(i);
+		if (source_object instanceof MetadataElement) {
+		    String full_element_name = ((MetadataElement)source_object).getFullName();
+		    if(full_element_name.startsWith(StaticStrings.EXTRACTED_NAMESPACE) && full_element_name.indexOf(StaticStrings.NS_SEP, StaticStrings.EXTRACTED_NAMESPACE.length()) == -1) {
+		       id_buffer.append(full_element_name.substring(StaticStrings.EXTRACTED_NAMESPACE.length()));
+		    }
+		    else {
+			id_buffer.append(full_element_name);
+		    }
+		}
+		else {
+		    id_buffer.append(source_object.toString());
+		}
+		id_buffer.append(StaticStrings.COMMA_CHARACTER);
+	    }
+	    sources = null;
+	    if (id_buffer.length()==0) {
+		id = "";
+	    } else {
+		id = id_buffer.substring(0, id_buffer.length() - 1);
+	    }
+	}
+	return id;
+    }
+
+
+    /** Retrieve the sources of this index.
+     * @return the sources as an ArrayList
+     */
+    public ArrayList getSources() {
+	if(sources == null) {
+	    sources = new ArrayList();
+	    NodeList content_elements = element.getElementsByTagName(StaticStrings.CONTENT_ELEMENT);
+	    int content_elements_length = content_elements.getLength();
+	    for(int i = 0; i < content_elements_length; i++) {
+		Element content_element = (Element) content_elements.item(i);
+		String metadata_element_name_full = (String) content_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+		MetadataElement metadata_element = MetadataTools.getMetadataElementWithName(metadata_element_name_full);
+		if (metadata_element != null) {
+		    sources.add(metadata_element);
+		}
+		else {
+		    sources.add(metadata_element_name_full);
+		}
+	    }
+	    content_elements = null;
+	}
+	return sources;
+    }
+
+    public boolean isAssigned() {
+	return (element != null && !element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR));
+    }
+
+    public void setAssigned(boolean assigned) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	}
+    }
+
+    public void setElement(Element element) {
+	this.element = element;
+	this.level = -1;
+	this.id = null;
+	this.sources = null;
+    }
+
+    /** Method to set the level of this index which can only be used for the default index. 
+     * @param new_level the new level as an int
+     */
+    public void setLevel(int new_level) {
+	// System.err.println("SetLevel(" + new_level + ")");
+	if(element != null && element.getNodeName().equals(StaticStrings.INDEX_DEFAULT_ELEMENT)) {
+	    if (0 <= new_level && new_level < 3) {
+		element.setAttribute(StaticStrings.LEVEL_ATTRIBUTE, LEVEL[new_level]);
+	    } else {
+		element.setAttribute(StaticStrings.LEVEL_ATTRIBUTE, "");
+	    }
+	    this.id = null; // Regenerate ID.
+	    this.level = new_level;
+	}
+	else {
+	    DebugStream.println("Error! Called setLevel() of index other than the default.");
+	}
+    }
+
+    /** Method to set the sources for this index which can only be used for the default index.
+     * @param sources an ArrayList of source names
+     */
+    public void setSources(ArrayList sources) {
+	if(element != null && element.getNodeName().equals(StaticStrings.INDEX_DEFAULT_ELEMENT)) {
+	    // Erase old sources
+	    XMLTools.clear(element);
+	    // For each entry in the sources array add a new content element.
+	    int size = sources.size();
+	    for(int i = 0; i < size; i++) {
+		Element content_element = element.getOwnerDocument().createElement(StaticStrings.CONTENT_ELEMENT);
+		Object source_object = sources.get(i);
+		if (source_object instanceof MetadataElement) {
+		    String name = ((MetadataElement) source_object).getFullName();
+		    content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+		    name = null;
+		}
+		else {
+		    //DebugStream.println("Found String as source: " + source_object.toString());
+		    content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, source_object.toString());
+		}
+		source_object = null;
+		element.appendChild(content_element);
+		content_element = null;
+	    }
+	    this.id = null; // Regenerate ID.
+	    this.sources = sources;
+	}
+	else {
+	    DebugStream.println("Error! Called setSource() of index other than the default.");
+	}
+    }
+
+    /** Method to turn this object into a string representation ready to be placed in the collection configuration file.
+     * @return A <strong>String</strong> containing the information of this class.
+     */
+    public String toString() {
+	StringBuffer text_buffer = new StringBuffer("");
+	// Generate language dependant id (include extracted metadata namespace)
+	// Write level information, if any.
+	int level = getLevel();
+	if(0 <= level && level < 3) {
+	    text_buffer.append(LEVEL[level]);
+	    text_buffer.append(StaticStrings.COLON_CHARACTER);
+	}
+	// Write data information. Retrieve each of the content sources and add them in a comma separated list.
+	ArrayList sources = getSources();
+	int sources_size = sources.size();
+	for(int i = 0; i < sources_size; i++) {
+	    String source_name = (sources.get(i)).toString();
+	    text_buffer.append(source_name);
+	    if(i < sources_size - 1) {
+		text_buffer.append(StaticStrings.COMMA_CHARACTER);
+	    }
+	}
+	sources = null;
+	return text_buffer.toString();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexManager.java	(revision 31635)
@@ -0,0 +1,1115 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.GComboBox;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.ModalDialog;
+import org.greenstone.gatherer.gui.SimpleMenuBar;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.util.CheckList;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.XMLTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.*;
+/** This class is resposible for storing the indexes which have been assigned to this collection and the default index, and providing methods for interacting with both these data pools. It also knows how to turn itself into a String as it would be displayed in the collection configuration file.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class IndexManager
+    extends DOMProxyListModel 
+    implements BuildTypeManager.BuildTypeListener {
+
+    /** A reference to ourselves so our inner methods have access. */
+    private DOMProxyListModel index_model = null;
+    /** The default index. */
+    private Index default_index = null;
+
+    private Control controls = null;
+    private String build_type = null;
+    
+    static final private Dimension FIELD_SIZE = new Dimension(200,30);
+    static final private String ALLFIELDS = "allfields";
+
+    static final private Dimension PROMPT_SIZE = new Dimension(400,400);
+    public IndexManager(Element indexes, String current_build_type) {
+
+	super(indexes, StaticStrings.INDEX_ELEMENT, new Index());
+	DebugStream.println("IndexManager: " + getSize() + " indexes parsed.");
+	index_model = this;
+      
+       
+	// Parse and retrieve the default index
+	NodeList default_index_elements = CollectionConfiguration.getElementsByTagName(StaticStrings.INDEX_DEFAULT_ELEMENT);
+	if(default_index_elements.getLength() > 0) {
+	             
+	    default_index = new Index((Element)default_index_elements.item(0));
+	}
+	build_type = current_build_type;
+    }
+
+    /** Method to add a new index.
+     * @param index The <strong>Index</strong> to add.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     */
+    private void addIndex(Index index, CollectionMeta metadatum) {
+	///ystem.err.println("Adding an index: " + index.toString());
+	if(!contains(index)) {
+	    CollectionDesignManager.collectionmeta_manager.addMetadatum(metadatum);
+	    // Retrieve the currently last index
+	    if(getSize() > 0) {
+		Index last_index = (Index)getElementAt(getSize() - 1);
+		addAfter(index, last_index);
+		
+	    }
+	    else {
+		add(index);
+		// Also set this index as the default one, 
+		setDefault(index);
+	    }
+	}
+	else {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.IndexManager.Index_Exists"), Dictionary.get("General.Warning"), JOptionPane.WARNING_MESSAGE);
+	}
+    }
+
+    public void buildTypeChanged(String new_build_type) {
+	if (build_type.equals(new_build_type)) {
+	    return;
+	}
+	// we don;t care about this if old or new is not MG as MGPP and 
+	// Lucene have the same index specification
+	if (!build_type.equals(BuildTypeManager.BUILD_TYPE_MG) && !new_build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+	    return;
+	}
+	boolean mg_to_mgpp = true;
+	if (new_build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+	    mg_to_mgpp = false;
+	}
+	if (mg_to_mgpp) {
+	    changeToMGPPIndexes();
+	} else {
+	    changeToMGIndexes();
+	}
+	build_type  = new_build_type;
+	// its really hard to transfer defaults between mgpp/lucene and mg indexes, so we'll just set the first one to be the default
+	Index first_index = (Index) getElementAt(0);
+	setDefault(first_index);
+	first_index = null;
+    }
+    
+    private void changeToMGIndexes() {
+	Element mgpp_element = root;
+	// Retrieve and assign MG element and default index element
+	Element mg_element = CollectionDesignManager.collect_config.getMGIndexes();
+	mg_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+	NodeList indexes = mg_element.getElementsByTagName(StaticStrings.INDEX_ELEMENT);
+	// Replace mgpp element with mg element
+	setRoot(mg_element);
+	
+	if(indexes.getLength() == 0) {
+	  // we need to create some based on the mgpp indexes
+	  
+	  // If the current mgpp index includes a text one, then generate text indexes for each of the registered levels.
+	  Index index = getIndex(StaticStrings.TEXT_STR);
+	  if(index != null) {
+	    ArrayList levels = CollectionDesignManager.index_manager.getLevels();
+	    int level_size = levels.size();
+	    for(int i = 0; i < level_size; i++) {
+	      IndexOption level = (IndexOption) levels.get(i);
+	      Index new_index = new Index(level.getName(), index.getSources());
+	      // Try to retrieve existing metadatum
+	      String source_str = new_index.getID();
+	      CollectionMeta metadatum = CollectionDesignManager.collectionmeta_manager.getMetadatum(StaticStrings.STOP_CHARACTER + source_str, false);
+	      // If no metadata was found, add new pseudo metadata using the id
+	      if(metadatum == null) {
+		metadatum = new CollectionMeta(StaticStrings.STOP_CHARACTER + source_str);
+		metadatum.setAssigned(true);
+		metadatum.setValue(source_str);
+	      }
+	      // If it was found, ensure it is assigned
+	      else {
+		metadatum.setAssigned(true);
+	      }
+	      source_str = null;
+	      addIndex(new_index, metadatum);
+	      new_index = null;
+	      level = null;
+	    }
+	  }
+	}
+	
+	// Unassign mgpp element 
+	mgpp_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+	mgpp_element = null;
+	
+    }
+
+    private void changeToMGPPIndexes() {
+	Element mg_element = root;
+	// Retrieve and assign the MGPP indexes element.
+	Element mgpp_element = CollectionDesignManager.collect_config.getMGPPIndexes();
+	mgpp_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+	NodeList indexes = mgpp_element.getElementsByTagName(StaticStrings.INDEX_ELEMENT);
+	if(indexes.getLength() != 0) {
+	    // we just reinstate the indexes we had before the change
+	    setRoot(mgpp_element);
+	} else {
+	    // If the MGPP indexes element is empty (ie was created by CollectionConfiguration), generate new MGPP index from the existing index
+	    
+	    ArrayList sources_list = new ArrayList();
+
+	    // We first use details from the default index if any
+	    if(default_index != null) {
+		ArrayList sources = default_index.getSources();
+		sources_list.addAll(sources);
+	    }
+	    int size = getSize();
+	    for(int i = 0; i < size; i++) {
+		Index index = (Index) getElementAt(i);
+		ArrayList sources = index.getSources();
+		sources.removeAll(sources_list);
+		sources_list.addAll(sources);
+		index = null;
+	    }
+	    // Replace mg element with mgpp element
+	    setRoot(mgpp_element);
+	    
+	    // We now have a list of sources, so create new indexes based on these
+	    int sources_list_size = sources_list.size();
+	    for(int j = 0; j < sources_list_size; j++) {
+		Object source_object = sources_list.get(j);
+		String source_str = null;
+		if(source_object instanceof MetadataElement) {
+		    source_str = ((MetadataElement) source_object).getFullName();
+		}
+		else {
+		    source_str = source_object.toString();
+		}
+		ArrayList new_sources = new ArrayList();
+		new_sources.add(source_object);
+		source_object = null;
+		Index new_index = new Index(new_sources);
+		// Try to retrieve existing metadatum
+		source_str = new_index.getID();
+		CollectionMeta metadatum = CollectionDesignManager.collectionmeta_manager.getMetadatum(StaticStrings.STOP_CHARACTER + source_str, false);
+		// If no metadata was found, add new pseudo metadata using the id
+		if(metadatum == null) {
+		    metadatum = new CollectionMeta(StaticStrings.STOP_CHARACTER + source_str);
+		    metadatum.setAssigned(true);
+		    metadatum.setValue(source_str);
+		}
+		// If it was found, ensure it is assigned
+		else {
+		    metadatum.setAssigned(true);
+		}
+		source_str = null;
+		addIndex(new_index, metadatum);
+		metadatum = null;
+		new_index = null;
+		new_sources = null;
+		source_str = null;
+	    }	      
+	    
+	}
+
+	// Unassign MG element
+	mg_element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+	mg_element = null;
+	
+    }
+    
+    public Control getControls() {
+	if (controls == null) {
+	    controls = new IndexControl();
+	}
+	return controls;
+    }
+
+    /** Method to retrieve a certain index, as referenced by an index number.
+     * @param index An <i>int</i> which indicates the position of the desired index.
+     * @return The <strong>Index</strong> at the given index, or <i>null</i> if no such index exists.
+     */
+    public Index getIndex(int index) {
+	if(0 <= index && index < getSize()) {
+	    return (Index)getElementAt(index);
+	}
+	return null;
+    }
+
+    /** Method to retrieve a certain index, given its id.
+     * @param id the id of the index as a String
+     * @return the Index that matches id, or null if no such index exists
+     */
+    public Index getIndex(String id) {
+	int size = getSize();
+	for(int i = 0; i < size; i++) {
+	    Index index = (Index) getElementAt(i);
+	    if(index.getID().equals(id)) {
+		return index;
+	    }
+	}
+	return null;
+    }
+
+    public ArrayList getIndexes() {
+	return children();
+    }
+
+
+
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+
+    }
+
+    private void moveIndex(Index index, boolean move_up)
+    {
+	// Determine the current position of the index
+	int position = indexOf(index);
+	// Determine if it can be moved, ie if its not already at the top trying to move up, or at the bottom trying to move down.
+	if(position == -1) {
+	    return;
+	}
+	if(position == 0 && move_up) {
+	    return;
+	}
+	if(position == (getSize()) - 1 && !move_up) {
+	    return;
+	}
+	
+	// Ok, move up
+	if (move_up) {
+	    position--;
+	    remove(index);
+	    add(position, index);
+	}
+
+	// Or move down
+	else {
+	    position++;
+	    remove(index);
+	    add(position, index);
+	}
+    }
+
+
+
+    /** Method to remove a certain index.
+     * @param index the Index to remove.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.cdm.CollectionDesignManager
+     * @see org.greenstone.gatherer.cdm.CollectionMetaManager
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     */
+    private void removeIndex(Index index) {
+	if(index != null) {
+	    // Remove any current metadata from this index
+	    CollectionDesignManager.collectionmeta_manager.removeMetadata(StaticStrings.STOP_CHARACTER + index.getID());
+	    // Remove the index
+	    remove(index);
+	    // Check if the index removed happens to be the default index
+	    if(default_index != null && default_index.equals(index)) {
+		// If so our first solution is to set the first index to be default
+		if(getSize() > 0) {
+		    Index another_index = (Index) getElementAt(0);
+		    setDefault(another_index);
+		    another_index = null;
+		}
+		else {
+		    default_index.setAssigned(false);
+		}
+	    }
+	}
+    }
+
+
+    /* replace an index in the list. new index may have the same sources but a
+       different name, or may be a new index altogether */
+    private void replaceIndex(Index old_index, Index new_index,
+			      CollectionMeta coll_meta) {
+	if (old_index == null || new_index == null || coll_meta == null) {
+	    return;
+	}
+	if (!old_index.getID().equals(new_index.getID()) && contains(new_index)) {
+	    // shoudl we output an error??
+	    return;
+	}
+	// Remove the old index coll meta
+	CollectionDesignManager.collectionmeta_manager.removeMetadata(StaticStrings.STOP_CHARACTER + old_index.getID());
+	// Add the new coll meta
+	CollectionDesignManager.collectionmeta_manager.addMetadatum(coll_meta);
+
+	// get the position of the old one
+	int position = indexOf(old_index);
+	// remove it
+	remove(old_index);
+	// add the new one at that position
+	add(position, new_index);
+    }
+
+
+    /** Method to set the default index.
+     * @param index the new default Index
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     */
+    public void setDefault(Index index) {
+	if(index != null) {
+	    if(default_index == null) {
+		// Create the default index element, and place immediately after indexes element.
+		Element default_index_element = root.getOwnerDocument().createElement(StaticStrings.INDEX_DEFAULT_ELEMENT);               
+		default_index = new Index(default_index_element);
+		Node target_node = CollectionConfiguration.findInsertionPoint(default_index_element);
+		if(target_node != null) {
+		    root.getOwnerDocument().getDocumentElement().insertBefore(default_index_element, target_node);
+		}
+		else {
+		    root.getOwnerDocument().getDocumentElement().appendChild(default_index_element);
+		}
+	    }
+	    default_index.setAssigned(true);
+	    default_index.setLevel(index.getLevel());
+	    default_index.setSources(index.getSources());
+	    
+	}
+	else {
+	    if(default_index != null) {
+		default_index.setAssigned(false);
+	    }
+	}
+    }
+
+
+    private class IndexControl
+	extends JPanel
+	implements Control {
+
+	private JList index_list;
+	private JButton move_down_button;
+	private JButton move_up_button;
+	private JButton set_default_button;
+
+	private JButton new_button;
+	private JButton edit_button;
+	private JButton remove_button;
+
+	public IndexControl() {
+	    super();
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    // Creation
+	    JPanel assigned_indexes_pane = new JPanel();
+	    assigned_indexes_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JLabel index_label = new JLabel(Dictionary.get("CDM.IndexManager.Indexes"));
+            index_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    index_list = new JList(index_model);
+	    index_list.setCellRenderer(new IndexListRenderer());
+	    index_list.setVisibleRowCount(6);
+	    index_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+            index_list.addMouseListener(new ClickListener());
+            index_list.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel movement_pane = new JPanel();
+            movement_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    move_up_button = new GLIButton(Dictionary.get("CDM.Move.Move_Up"), JarTools.getImage("arrow-up.gif"), Dictionary.get("CDM.Move.Move_Up_Tooltip"));
+	    move_up_button.setEnabled(false);	    
+            
+	    move_down_button = new GLIButton(Dictionary.get("CDM.Move.Move_Down"), JarTools.getImage("arrow-down.gif"), Dictionary.get("CDM.Move.Move_Down_Tooltip"));
+	    move_down_button.setEnabled(false);
+	    	    
+	    set_default_button = new GLIButton(Dictionary.get("CDM.IndexManager.Set_Default"), Dictionary.get("CDM.IndexManager.Set_Default_Tooltip"));
+	    set_default_button.setEnabled(false);
+	    
+	    JPanel button_pane = new JPanel();
+	    button_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+            new_button = new GLIButton(Dictionary.get("CDM.IndexManager.New_Index")+StaticStrings.FURTHER_DIALOG_INDICATOR, Dictionary.get("CDM.IndexManager.New_Index_Tooltip"));
+	    new_button.setEnabled(true);
+	    
+	    edit_button = new GLIButton(Dictionary.get("CDM.IndexManager.Edit_Index")+StaticStrings.FURTHER_DIALOG_INDICATOR, Dictionary.get("CDM.IndexManager.Edit_Index_Tooltip"));
+	    edit_button.setEnabled(false);
+	    
+	    remove_button = new GLIButton(Dictionary.get("CDM.IndexManager.Remove_Index"), Dictionary.get("CDM.IndexManager.Remove_Index_Tooltip"));
+	    remove_button.setEnabled(false);
+	    	  
+	    // Listeners
+	    new_button.addActionListener(new NewIndexListener());
+	    edit_button.addActionListener(new EditIndexListener());
+	    remove_button.addActionListener(new RemoveIndexListener());
+	    remove_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+
+	    index_list.addListSelectionListener(new IndexListListener());
+
+	    move_down_button.addActionListener(new MoveListener(false));
+	    move_down_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    move_up_button.addActionListener(new MoveListener(true));
+	    move_up_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+
+	    set_default_button.addActionListener(new SetDefaultListener());
+	    // Layout
+	    movement_pane.setBorder(BorderFactory.createEmptyBorder(0,2,0,0));
+	    movement_pane.setLayout(new GridLayout(3,1));
+	    movement_pane.add(move_up_button);
+	    movement_pane.add(move_down_button);
+	    movement_pane.add(set_default_button);
+
+	    assigned_indexes_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+	    assigned_indexes_pane.setLayout(new BorderLayout());
+	    assigned_indexes_pane.add(index_label, BorderLayout.NORTH);
+	    assigned_indexes_pane.add(new JScrollPane(index_list), BorderLayout.CENTER);
+	    assigned_indexes_pane.add(movement_pane, BorderLayout.LINE_END);
+
+	    button_pane.setLayout(new GridLayout(1,3,5,0));
+	    button_pane.add(new_button);
+	    button_pane.add(edit_button);
+	    button_pane.add(remove_button);
+
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(assigned_indexes_pane, BorderLayout.CENTER);
+	    add(button_pane, BorderLayout.SOUTH);
+	    
+	}
+	public void loseFocus() {}
+	public void gainFocus() {}
+	public void destroy() {}
+  
+	/** Listens for double clicks apon the list and react as if the configure button was pushed. */
+	private class ClickListener
+	    extends MouseAdapter {
+	    /** Called whenever the mouse is clicked over a registered component, we use this to chain through to the configure prompt.
+	     * @param event A <strong>MouseEvent</strong> containing information about the mouse click.
+	    */
+    public void mouseClicked(MouseEvent event) {
+      if(event.getClickCount() == 2 ) {
+        if(!index_list.isSelectionEmpty()) {
+          Index index = (Index) index_list.getSelectedValue();
+          NewIndexPrompt nip = new NewIndexPrompt(build_type, index);
+          nip.destroy();          
+        }
+      }
+    }
+  }
+
+  
+	private class IndexListListener
+	    implements ListSelectionListener {
+	    
+	    /** This method is called whenever the source list selection changes. When it does we need to fill in the various parts of the list description panel
+	     * @param event A <strong>ListSelectionEvent</strong> containing further information about the list selection.
+	     */
+	    public void valueChanged(ListSelectionEvent event)
+	    {
+		if (event.getValueIsAdjusting()) {
+		    return;
+		}
+		
+                set_default_button.setEnabled(true);
+		Object value = index_list.getSelectedValue();
+		if (value == null) {
+		    move_down_button.setEnabled(false);
+		    move_up_button.setEnabled(false);
+		    remove_button.setEnabled(false);
+		    edit_button.setEnabled(false);
+		    set_default_button.setEnabled(false);
+		    return;
+		}
+
+		// Enable the buttons appropriately
+		remove_button.setEnabled(true);
+		edit_button.setEnabled(true);
+		set_default_button.setEnabled(default_index == null || !default_index.equals(value));
+		int i = index_list.getSelectedIndex();
+		int size = index_list.getModel().getSize();
+		move_up_button.setEnabled((i>0));
+		move_down_button.setEnabled((i<size-1));
+	    }
+	}
+
+	private class IndexListRenderer
+	    extends DefaultListCellRenderer {
+
+	    /** Return a component that has been configured to display the specified value. */
+	    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+		JLabel component = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+		if(default_index != null && default_index.equals(value)) {
+		    component.setText(component.getText() + " " + Dictionary.get("CDM.IndexManager.Default_Index_Indicator"));
+		}
+		return component;
+	    }
+
+	}
+
+	private class NewIndexListener
+	    implements ActionListener {
+
+	    public void actionPerformed(ActionEvent event) {
+		NewIndexPrompt nip = new NewIndexPrompt(build_type, null);
+		nip.destroy();
+	    }
+	}
+	
+	private class EditIndexListener
+	    implements ActionListener {
+
+	    public void actionPerformed(ActionEvent event) {
+		Index index = (Index) index_list.getSelectedValue();
+		NewIndexPrompt nip = new NewIndexPrompt(build_type, index);
+		nip.destroy();
+	    }
+	}
+
+	private class RemoveIndexListener
+	    implements ActionListener {
+	    
+	    public void actionPerformed(ActionEvent event) {
+		
+		int i = index_list.getSelectedIndex();
+		if (i != -1) {
+		    removeIndex((Index) index_list.getSelectedValue());
+		}
+		int size = index_list.getModel().getSize();
+		if (i == size) {
+		    i--;
+		}
+		index_list.setSelectedIndex(i);
+		// This will produce an event on the list, updating the other buttons
+	    }
+	}
+
+	private class MoveListener
+	    implements ActionListener {
+
+	    private boolean move_up;
+
+	    public MoveListener(boolean move_up) {
+		this.move_up = move_up;
+	    }
+
+	    public void actionPerformed(ActionEvent event) {
+		// Retrieve the selected index
+		Index index = (Index) index_list.getSelectedValue();
+		if(index != null) {
+		    moveIndex(index, move_up);
+		    // Ensure the index that moved is still selected
+		    index_list.setSelectedValue(index, true);
+		    index = null;
+		}
+	    }
+	}
+
+	private class SetDefaultListener
+	    implements ActionListener {
+
+	    public void actionPerformed(ActionEvent event) {
+		Index index = (Index) index_list.getSelectedValue();
+		if(index != null) {
+		    setDefault(index);
+		    // This should cause a repaint of just the desired row
+		    index_list.setSelectedValue(index, true);
+		}
+		set_default_button.setEnabled(false);
+	    }
+	}
+
+	
+
+	private class NewIndexPrompt 
+	    extends ModalDialog {
+
+	    NewIndexPrompt new_index_prompt = null;
+	
+	    private JCheckBox text_checkbox;
+	    private CheckList source_list;
+	    // mg uses a level box
+	    private JComboBox level_combobox;
+	    // mgpp has a allfields selector
+	    private JCheckBox allfields_box;
+
+	    private JButton add_or_replace_button;
+	    private JButton select_all_button;
+	    private JButton select_none_button;
+	    private JButton add_all_button;
+	    //private JButton select_all_button;
+	    //private JButton replace_button;
+	    private JButton cancel_button;
+
+	    private boolean mgpp_enabled = false;
+	    private boolean editing = false;
+	
+	    public NewIndexPrompt(String build_type, Index existing_index) {
+		super(Gatherer.g_man, true);
+                this.setComponentOrientation(Dictionary.getOrientation());
+                
+		new_index_prompt = this;
+
+		setModal(true);
+		setSize(PROMPT_SIZE);
+		if (existing_index != null) {
+		    setTitle (Dictionary.get("CDM.IndexManager.Edit_Index"));
+		    editing = true;
+		} else {
+		    setTitle(Dictionary.get("CDM.IndexManager.New_Index"));
+		}
+		
+		setJMenuBar(new SimpleMenuBar("searchindexes"));
+		if (build_type.equals(BuildTypeManager.BUILD_TYPE_MGPP) || build_type.equals(BuildTypeManager.BUILD_TYPE_LUCENE)) {
+		    mgpp_enabled = true;
+		}
+		JPanel content_pane = (JPanel)this.getContentPane();
+                content_pane.setComponentOrientation(Dictionary.getOrientation());
+		ArrayList new_data = new ArrayList();
+		new_data.addAll(MetadataSetManager.getEveryMetadataSetElement());
+
+		JPanel details_pane = new JPanel();
+                details_pane.setComponentOrientation(Dictionary.getOrientation());
+		
+		text_checkbox = new JCheckBox(Dictionary.get("CDM.IndexManager.Text_Source"));
+		text_checkbox.setToolTipText(Dictionary.get("CDM.IndexManager.Text_Source_Tooltip"));
+                text_checkbox.setComponentOrientation(Dictionary.getOrientation());                
+		text_checkbox.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+			    validateAddOrReplaceButton();
+			}
+		    });
+		JLabel source_label = new JLabel(Dictionary.get("CDM.IndexManager.Source"));
+                source_label.setComponentOrientation(Dictionary.getOrientation());
+		source_list = new CheckList(false);                
+		source_list.setListData(new_data);
+		source_list.setToolTipText(Dictionary.get("CDM.IndexManager.Source_Tooltip"));
+		source_list.addListSelectionListener(new SourceListListener());
+
+		JPanel button_pane = new JPanel();
+		button_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+		button_pane.setComponentOrientation(Dictionary.getOrientation());
+                
+		if (existing_index == null && mgpp_enabled) {
+		    button_pane.setLayout(new GridLayout(2,3,5,0));
+                    JPanel tmp = new JPanel();
+                    tmp.setComponentOrientation(Dictionary.getOrientation());
+		    button_pane.add(tmp);
+		} else {
+		    button_pane.setLayout(new GridLayout(2,2,5,0));
+		}
+		
+		select_all_button = new GLIButton(Dictionary.get("CDM.IndexManager.Select_All"), Dictionary.get("CDM.IndexManager.Select_All_Tooltip"));
+		select_all_button.addActionListener(new ActionListener() {
+			
+			public void actionPerformed(ActionEvent event) {
+			    text_checkbox.setSelected(true);
+			    source_list.setAllTicked();
+			    validateAddOrReplaceButton();
+			}
+		    });
+		
+		select_none_button = new GLIButton(Dictionary.get("CDM.IndexManager.Select_None"), Dictionary.get("CDM.IndexManager.Select_None_Tooltip"));
+		select_none_button.addActionListener(new ActionListener() {
+			
+			public void actionPerformed(ActionEvent event) {
+			    text_checkbox.setSelected(false);
+			    source_list.clearTicked();
+			    validateAddOrReplaceButton();
+			}
+		    });
+		
+		
+		button_pane.add(select_all_button);
+		button_pane.add(select_none_button);
+		
+		if (existing_index != null) {
+		    add_or_replace_button = new GLIButton(Dictionary.get("CDM.IndexManager.Replace_Index"), Dictionary.get("CDM.IndexManager.Replace_Index_Tooltip"));
+		    add_or_replace_button.addActionListener(new ReplaceIndexListener());
+		} else {
+		    add_or_replace_button = new GLIButton(Dictionary.get("CDM.IndexManager.Add_Index"), Dictionary.get("CDM.IndexManager.Add_Index_Tooltip"));
+		    add_or_replace_button.addActionListener(new AddIndexListener());
+		}
+
+		add_or_replace_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+		add_or_replace_button.setEnabled(false);
+		button_pane.add(add_or_replace_button);
+		
+		if (existing_index == null && mgpp_enabled) {
+		    add_all_button = new GLIButton(Dictionary.get("CDM.IndexManager.Add_All"), Dictionary.get("CDM.IndexManager.Add_All_Tooltip"));
+		    add_all_button.setEnabled(true);
+		    add_all_button.addActionListener(new AddAllIndexActionListener());
+		    button_pane.add(add_all_button);
+		} 
+		
+		// always have a cancel button
+		cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel"));
+		cancel_button.setEnabled(true);
+		cancel_button.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+			    new_index_prompt.dispose();
+			}
+		    });
+		button_pane.add(cancel_button);
+
+		//Layout
+		details_pane.setLayout(new BorderLayout(10,10));
+		details_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+		details_pane.add(text_checkbox, BorderLayout.NORTH);
+		details_pane.add(new JScrollPane(source_list), BorderLayout.CENTER);
+
+
+		// do type specific stuff
+		if (mgpp_enabled) {
+		    // allfields
+		    allfields_box = new JCheckBox(Dictionary.get("CDM.IndexManager.Allfields_Index"));
+		    allfields_box.addItemListener(new AllFieldsBoxListener());
+		    allfields_box.setComponentOrientation(Dictionary.getOrientation());
+                    //JLabel allfields_label = new JLabel(Dictionary.get("CDM.IndexManager.Allfields_Index"));
+		    details_pane.add(allfields_box, BorderLayout.SOUTH);
+		    
+		    
+		} else {
+		    // index level
+		    JLabel level_label = new JLabel(Dictionary.get("CDM.IndexManager.Level"));
+		    level_label.setComponentOrientation(Dictionary.getOrientation());
+                    
+                    level_combobox = new JComboBox();
+		    level_combobox.setOpaque(false);
+		    level_combobox.setPreferredSize(FIELD_SIZE);
+		    // Note the order of these must be the same as the
+		    // level order in Index
+		    level_combobox.addItem(StaticStrings.DOCUMENT_STR);//Dictionary.get("CDM.LevelManager.Document"));
+		    level_combobox.addItem(StaticStrings.SECTION_STR);//Dictionary.get("CDM.LevelManager.Section"));
+		    level_combobox.addItem(StaticStrings.PARAGRAPH_STR);//Dictionary.get("CDM.LevelManager.Paragraph"));
+		    level_combobox.setEditable(false);
+		    level_combobox.setToolTipText(Dictionary.get("CDM.IndexManager.Level_Tooltip"));
+		    level_combobox.setComponentOrientation(Dictionary.getOrientation());
+                    level_combobox.addActionListener(new ActionListener() {
+			    public void actionPerformed(ActionEvent event) {
+				validateAddOrReplaceButton();
+			    }
+			});
+		    JPanel level_pane = new JPanel();
+                    level_pane.setComponentOrientation(Dictionary.getOrientation());
+		    level_pane.setLayout(new BorderLayout());
+		    level_pane.add(level_label, BorderLayout.LINE_START);
+		    level_pane.add(level_combobox, BorderLayout.CENTER);
+		    details_pane.add(level_pane, BorderLayout.SOUTH);
+
+		}
+		// if we are editing, fill in the controls
+		if (existing_index !=null) {
+		    ArrayList sources = existing_index.getSources();
+		    if (mgpp_enabled && sources.get(0).equals(ALLFIELDS)) {
+			allfields_box.setSelected(true);
+			source_list.setEnabled(false);
+		    } else {
+			source_list.setTickedObjects(sources.toArray());
+			source_list.setEnabled(true);
+			if (sources.contains(StaticStrings.TEXT_STR)) {
+			    text_checkbox.setSelected(true);
+			}
+		    } 
+		    if (!mgpp_enabled) {
+			level_combobox.setSelectedIndex(existing_index.getLevel());
+		    }
+		    
+		}
+		content_pane.setLayout(new BorderLayout());
+		content_pane.add(source_label, BorderLayout.NORTH);
+		content_pane.add(details_pane, BorderLayout.CENTER);
+		content_pane.add(button_pane, BorderLayout.SOUTH);
+	    
+		// Display on screen.
+		Dimension screen_size = Configuration.screen_size;
+		setLocation((screen_size.width - PROMPT_SIZE.width) / 2, (screen_size.height - PROMPT_SIZE.height) / 2);
+		screen_size = null;
+		setVisible(true);
+	    
+	    }
+
+	    /** Method which actually forces the dialog to be shown on screen.
+	     * @return <i>true</i> if the user completed configuration and pressed ok, <i>false</i> otherwise.
+	     */
+	    public boolean display() {
+		setVisible(true);
+		return true;
+		//    return success;
+	    }
+	
+	    public void destroy() {
+	    }
+
+	    // Checks that specified index not already in the collection
+	    private void validateAddOrReplaceButton() {
+		Index index;
+		ArrayList sources;
+		if (mgpp_enabled && allfields_box.isSelected()) {
+		    sources = new ArrayList();
+		    sources.add(ALLFIELDS);
+		    index = new Index(sources);
+		    
+		} else if (text_checkbox.isSelected() || 
+			   !source_list.isNothingTicked()) {
+		    sources = source_list.getTicked();
+		    if (text_checkbox.isSelected()) {
+			sources.add(0, StaticStrings.TEXT_STR);
+		    }		    
+		    if (mgpp_enabled) {
+			index = new Index(sources);
+		    } else {
+			index = new Index(level_combobox.getSelectedIndex(), sources);
+		    }
+		} else {
+		    // nothing selected
+		    add_or_replace_button.setEnabled(false);
+		    return;
+		}
+		
+		sources = null;
+		if (index_model.contains(index)) {
+		    add_or_replace_button.setEnabled(false);
+		}
+		else {
+		    add_or_replace_button.setEnabled(true);
+		}
+		
+	    }
+	    private class AddIndexListener
+		implements ActionListener
+	    {
+		public void actionPerformed(ActionEvent event)
+		{
+		    Index index;
+		    if (mgpp_enabled && allfields_box.isSelected()) {
+			ArrayList sources = new ArrayList();
+			sources.add(ALLFIELDS);
+			index = new Index(sources);
+		    }
+		    else if (text_checkbox.isSelected() || 
+			     !source_list.isNothingTicked()) {
+			ArrayList sources = source_list.getTicked();
+			if (text_checkbox.isSelected()) {
+			    sources.add(0, StaticStrings.TEXT_STR);
+			}
+			if(mgpp_enabled) {
+			    index = new Index(sources);
+			} else {
+			    index = new Index(level_combobox.getSelectedIndex(), sources);
+			}
+		    } else {
+			return;
+		    }
+		 
+		    // Before we add the index to the model, we have to add a default collection metadata for this
+		    CollectionMeta metadatum = new CollectionMeta(StaticStrings.STOP_CHARACTER + index.getID());
+		    if (use_macro_as_display_name(index.getID())) {
+			metadatum.setValue(get_macro_name(index.getID()));
+		    } else {
+			metadatum.setValue(index.getID());
+		    }
+		
+		    // Finally, add the index
+		    addIndex(index, metadatum);
+		    index_list.setSelectedValue(index, true);
+		    new_index_prompt.dispose();
+		}
+	    }
+
+	    /** add all sources as separate indexes (fields).  */
+	    private class AddAllIndexActionListener
+		implements ActionListener {
+
+		public void actionPerformed(ActionEvent event) {
+		    ArrayList all_sources = source_list.getAll();
+		    all_sources.add(0, StaticStrings.TEXT_STR);
+		    ArrayList new_sources = new ArrayList();
+		    for(int i = 0; i < all_sources.size(); i++) {
+			Object source = all_sources.get(i);
+
+			// Create new index
+			new_sources.clear();
+			new_sources.add(source);
+			Index index = new Index(new_sources);
+			if(!index_model.contains(index)) {
+			    // Determine the metadatum value
+			    String name = source.toString();
+			    if(name.startsWith(StaticStrings.EXTRACTED_NAMESPACE) && name.indexOf(StaticStrings.NS_SEP, StaticStrings.EXTRACTED_NAMESPACE.length()) == -1) {
+				name = name.substring(StaticStrings.EXTRACTED_NAMESPACE.length());
+			    }
+			    // Create new metadatum
+			    CollectionMeta metadatum = new CollectionMeta(StaticStrings.STOP_CHARACTER + index.getID());
+			    metadatum.setValue(name);
+			    name = null;
+			    // Assign new index
+			    addIndex(index, metadatum);
+			}
+			source = null;
+			index = null;
+		    }
+		    new_sources = null;
+		    new_index_prompt.dispose();
+		
+		}
+	    }
+    
+	    private class AllFieldsBoxListener
+		implements ItemListener {
+		
+		public void itemStateChanged(ItemEvent event) {
+		    if (event.getStateChange() == ItemEvent.SELECTED) {
+			source_list.setEnabled(false);
+		    } else if (event.getStateChange() == ItemEvent.DESELECTED) {
+			source_list.setEnabled(true);
+		    }
+		    validateAddOrReplaceButton();
+		}
+		
+	    }
+
+	    private class SourceListListener
+		implements ListSelectionListener {
+	    
+		public void valueChanged(ListSelectionEvent event) {
+		    if (event.getValueIsAdjusting()) {
+			return;
+		    }
+		    validateAddOrReplaceButton();
+		}
+	    }
+	
+
+	    private class ReplaceIndexListener
+		implements ActionListener {
+	    
+		public void actionPerformed(ActionEvent event) {
+		
+		    ArrayList sources;
+		    Index index = null;
+		    if (text_checkbox.isSelected() || !source_list.isNothingTicked()) {
+			sources = source_list.getTicked();
+			if (text_checkbox.isSelected()) {
+			    sources.add(0, StaticStrings.TEXT_STR);
+			}
+			if (mgpp_enabled) {
+			    index = new Index(sources);
+			} else {
+			    index = new Index(level_combobox.getSelectedIndex(), sources);
+			}
+		    }
+		    if (index != null) {
+			// Create the new collection meta
+			CollectionMeta metadatum = new CollectionMeta(StaticStrings.STOP_CHARACTER + index.getID());
+			if (use_macro_as_display_name(index.getID())) {
+			    metadatum.setValue(get_macro_name(index.getID()));
+			} else {
+			    metadatum.setValue(index.getID());
+			}
+		    
+			// replace the index
+			replaceIndex((Index) index_list.getSelectedValue(), index, metadatum);
+			index_list.setSelectedValue(index, true);
+		    
+		    }
+		    new_index_prompt.dispose();
+		
+		}
+	    }
+
+	    /**
+	     * If the index is built with DC metadata, use macro variable as the display text, 
+	     * so that translations of DC metadata can be displayed in the search field drop-down list 
+	     * when interface language changes.
+	     *   
+	     * @param index_id Current index id.
+	     * @return Whether macro variable should be used.
+	     */
+	    private boolean use_macro_as_display_name(String index_id) {
+		//String metaname = index_id;	
+		if (index_id.indexOf(":") != -1) {
+		    index_id = index_id.substring(index_id.indexOf(":") + 1);
+		}
+		
+		String field = null;
+		String[] fields = index_id.split(",");
+		for (int i = 0; i < fields.length; i++) {
+		    String s = fields[i];
+		    if (s.indexOf(".") != -1) {	  
+			s = s.substring(s.indexOf(".") + 1);
+		    }
+		    if (field == null) {
+			field = s;
+		    }
+		    else if (!field.equals(s)) {
+			return false;
+		    }
+		}
+				
+		field = field.toLowerCase();
+		if (field.equals("text") || field.equals("title") || field.equals("creator") || field.equals("subject") || 
+		    field.equals("description") || field.equals("publisher") || field.equals("contributor") || field.equals("date") || 
+		    field.equals("type") || field.equals("format") || field.equals("identifier") || field.equals("source") || 
+		    field.equals("language") || field.equals("relation") || field.equals("coverage") || field.equals("rights")) {			  
+		    return true;
+		}
+		
+		return false;
+	    }
+
+	    /**
+	     * Get the corresponding macro variable name, eg. _labelTitle_, _labelCreator_.
+	     *   
+	     * @param index_id Current index id.
+	     * @return Name of the macro variable.
+	     */
+	    private String get_macro_name(String index_id) {
+		if (index_id.indexOf(":") != -1) {
+		    index_id = index_id.substring(index_id.indexOf(":") + 1);
+		}
+		if (index_id.indexOf(",") != -1) {
+		    index_id = index_id.substring(0, index_id.indexOf(","));
+		}
+		if (index_id.indexOf(".") != -1) {	  
+		    index_id = index_id.substring(index_id.indexOf(".") + 1);
+		}	
+		
+		return "_label" + index_id + "_";		
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexOption.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexOption.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexOption.java	(revision 31635)
@@ -0,0 +1,153 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Katherine Don, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.*;
+
+
+/** This class represents an index option. currently, might be a level or a stem option. 
+ */
+public class IndexOption
+    implements DOMProxyListEntry {
+
+    /** The Element this object will source its information from. */
+    private Element element;
+
+    /** Constructor used only during DOMProxyListModel initialization. */
+    public IndexOption() {
+    }
+
+    /** Normal constructor.
+     * @param  element the Element this object will find its data from
+     */
+    public IndexOption(Element element) {
+	this.element = element;
+    }
+
+    /** Creation of a brand new IndexOption 
+     * @param  name the name of this type as a String
+     */
+    public IndexOption(String name) {
+	this.element = CollectionConfiguration.createElement(StaticStrings.INDEXOPTION_ELEMENT);
+	this.element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+    }
+
+    /** Compare two objects for ordering. 
+     * @param  object the other Object to compare to
+     * @return <0 if this option should be before the given object, 0 if they are equal, and >0 if it should be after
+     */
+    public int compareTo(Object object) {
+	if(object == null) {
+	    return -1;
+	}
+	// hope we never compare a IndexOption with something that is not a IndexOption!
+	return getName().compareTo(((IndexOption)object).getName());
+    }
+
+    /** Factory-like method to allow DOMProxyListModel to generate new entries.
+     * @param  element the Element the new option should be based on
+     * @return a newly created DOMProxyListEntry for the given element
+     */
+    public DOMProxyListEntry create(Element element) {
+	return new IndexOption(element);
+    }
+
+    /** Determine if this option is equivalent to another object.
+     * @param  object the other Object to match against
+     * @return true if the two are equal, false otherwise
+     */
+    public boolean equals(Object object) {
+	return (compareTo(object) == 0);
+    }
+
+    /** Retrieve the element this DOMProxyListEntry is based upon. Specified by the interface.
+     * @return the Element in question
+     */
+    public Element getElement() {
+	return element;
+    }
+
+    /** Retrieve the option name
+     * @return the name as a String
+     */
+    public String getName() {
+	return element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+    }
+    /** Retrieve the option value
+     * @return the value as a string
+     */
+    public String getValue() {
+	return element.getAttribute(StaticStrings.VALUE_ATTRIBUTE);
+    }
+
+   /** Determine is this command has been assigned, either because it already existed in the collection configuration, or because it has been explicitly set by the user. Non-assigned entries imply they have been added by the GLI to ensure consistancy (and avoid NPE's!)
+     * @return true if this command has been assigned, false otherwise
+     */
+    public boolean isAssigned() {
+	return (element != null && !element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR));
+    }
+    
+    /** Set the assigned state.
+     * @param  assigned the desired state of assigned as a boolean
+     */
+    public void setAssigned(boolean assigned) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	}
+    }
+
+    /** Set the element that this DOMProxyListEntry is base on.
+     * @param  element the new Element that this entry should source informatin from
+     */
+    public void setElement(Element element) {
+	this.element = element;
+    }
+
+    /** Set the name of this option.
+     * @param  name the new name for this option, as a String
+     */
+    public void setName(String name) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+	}
+    }
+
+    /** Set the value of this option  - sometimes value is used as well as name*/
+    public void setValue(String value) {
+	if (element != null) {
+	    element.setAttribute(StaticStrings.VALUE_ATTRIBUTE, value);
+	}
+    }
+
+    /** Produce a text representation of this option
+     * @return a String showing this option's name
+     */
+    public String toString() {
+	return getName();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexOptionList.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexOptionList.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexOptionList.java	(revision 31635)
@@ -0,0 +1,72 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Katherine Don, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import org.w3c.dom.Element;
+import org.greenstone.gatherer.util.StaticStrings;
+
+/** A DOMProxyListModel thatholds options for indexoptions. For example, levels and stem options */
+public class IndexOptionList
+    extends DOMProxyListModel {
+
+    /** A reference to ourselves so our inner methods have access. */
+    private DOMProxyListModel options_model = null;
+
+    public IndexOptionList(Element options) {
+	super(options, StaticStrings.INDEXOPTION_ELEMENT, new IndexOption());
+
+	options_model = this;
+    }
+
+    public void addOption(String name) {
+	if (getOption(name) == null) {
+	    add(new IndexOption(name));
+	}
+    }
+
+
+    public IndexOption getOption(String name) {
+	int options_model_size = options_model.getSize();
+	for(int i = 0; i < options_model_size; i++) {
+	    IndexOption option = (IndexOption) options_model.getElementAt(i);
+	    if(option.getName().equals(name)) {
+		return option;
+	    }
+	}
+	return null;
+    }
+
+    public void removeOption(String name) {
+	IndexOption option_elem = null;
+	if ((option_elem = getOption(name)) != null) {
+	    options_model.remove(option_elem);
+	}
+    }
+
+
+}
+	    
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexOptionManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexOptionManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexOptionManager.java	(revision 31635)
@@ -0,0 +1,557 @@
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.*;
+
+
+public class IndexOptionManager 
+    implements BuildTypeManager.BuildTypeListener {
+
+    // we have an IndexOptionList (DOMProxyListModel) for each option type
+    private IndexOptionList level_model = null; 
+    private IndexOptionList options_model = null; 
+    /** the default level option  - for mgpp and lucene */
+    private IndexOption default_level = null;
+    
+    private String build_type = null;
+    private Control controls = null;
+    
+    public IndexOptionManager(String current_build_type) {
+	level_model = new IndexOptionList(CollectionDesignManager.collect_config.getLevels());
+	options_model = new IndexOptionList(CollectionDesignManager.collect_config.getIndexOptions());
+	
+	DebugStream.println("IndexOptionManager: " + level_model.getSize() + " levels parsed.");
+	DebugStream.println("IndexOptionManager: " + options_model.getSize() + " index options parsed.");
+	
+	build_type = current_build_type;
+	if (build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+	    level_model.setAssigned(false);
+	}
+	//	else if (build_type.equals(BuildTypeManager.BUILD_TYPE_LUCENE)) {
+	//   options_model.setAssigned(false);
+	//}
+	// Parse and retrieve the default level 
+	Element default_level_element = CollectionDesignManager.collect_config.getLevelDefault();
+	default_level = new IndexOption(default_level_element);
+	if (build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+	    default_level.setAssigned(false);
+	}
+	
+    }
+	
+    
+    public void buildTypeChanged(String new_build_type) {
+	boolean levels_enabled = true;
+	//boolean stem_enabled = true;
+	if (new_build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+	    levels_enabled = false;
+	}// else if (new_build_type.equals(BuildTypeManager.BUILD_TYPE_LUCENE)) {
+	 //   stem_enabled = false;
+	//}
+	level_model.setAssigned(levels_enabled);
+	default_level.setAssigned(levels_enabled);
+	//options_model.setAssigned(stem_enabled);
+	((IndexOptionControl)controls).buildTypeChanged(new_build_type);
+	build_type = new_build_type;
+    }
+
+    /** Method to acquire the controls for editing the indexes.
+     * @return the Control
+     */
+    public Control getControls() {
+	if(controls == null) {
+	    // Build controls
+	    controls = new IndexOptionControl();
+	}
+	return controls;
+    }
+    
+    public int getNumLevels() {
+	return level_model.getSize();
+    }
+    public ArrayList getLevels() {
+	return level_model.children();
+    }
+    
+    private void addLevel(String level) {
+	if (level_model.getOption(level) == null) {
+	    int position = 0;
+	    
+	    if (level.equals(StaticStrings.SECTION_STR) && level_model.getSize()==1) {
+		position = 1;
+	    }
+	    level_model.add(position, new IndexOption(level));
+	    // add in a default collection meta
+	    CollectionMeta meta = new CollectionMeta(StaticStrings.STOP_CHARACTER + level);
+	    meta.setAssigned(true);
+	    if (level.equals("document") || level.equals("section")) {
+		meta.setValue("_text" + level + "_");
+	    } else {	    
+		meta.setValue(level);
+	    }
+	    CollectionDesignManager.collectionmeta_manager.addMetadatum(meta);
+	}
+    }
+
+    private IndexOption getLevel(String level) {
+	return level_model.getOption(level);
+    }
+
+    private void removeLevel(String level_name) {
+	// Remove any current metadata from this level
+	CollectionDesignManager.collectionmeta_manager.removeMetadata(StaticStrings.STOP_CHARACTER + level_name);
+	// Remove the level
+	level_model.removeOption(level_name);
+    }
+
+   /** Method to set the default level
+     * @param level the new default level
+     */
+    private void setDefaultLevel(String level) {
+ 	if(level != null) {
+	    default_level.setValue(level);
+	    if (!build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+		default_level.setAssigned(true);
+	    } 
+	} else {
+	    default_level.setAssigned(false);
+	}
+    }
+
+    private class IndexOptionControl
+	extends JPanel
+	implements Control {
+
+	public StemmingControl stem_control = null;
+	public LevelControl level_control = null;
+	
+	public IndexOptionControl() {
+            
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    stem_control = new StemmingControl();
+	    level_control = new LevelControl();
+            
+	    setLayout(new BorderLayout());
+	    //if (!build_type.equals(BuildTypeManager.BUILD_TYPE_LUCENE)) {
+		add(stem_control, BorderLayout.NORTH);
+		//}
+	    if (!build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+		add(level_control, BorderLayout.CENTER);
+	    }
+	}
+
+	public void destroy() {}
+	public void gainFocus() {}
+	public void loseFocus() {}
+
+	public void buildTypeChanged(String new_build_type)
+	{
+	    if (build_type.equals(new_build_type)) {
+		// shouldn't happen
+		return;
+	    }
+	    // tell our sub parts about the change in build type
+	    stem_control.buildTypeChanged(new_build_type);
+	    level_control.buildTypeChanged(new_build_type);
+	    
+	    // then display/hide as appropriate
+	    //if (build_type.equals(BuildTypeManager.BUILD_TYPE_LUCENE)) {
+		// Changing to MG/MGPP
+	    //	add(stem_control, BorderLayout.NORTH);
+	    //}
+	    //else if (new_build_type.equals(BuildTypeManager.BUILD_TYPE_LUCENE)) {
+		// Changing to Lucene
+		//remove(stem_control);
+	    //}
+	    if (build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+		// changing from MG
+		add(level_control, BorderLayout.CENTER);
+	    }
+	    else if (new_build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+		// changing to MG
+		remove(level_control);
+	    }
+	    
+	}
+	    
+    }
+    
+    private class StemmingControl
+	extends JPanel
+	implements BuildTypeManager.BuildTypeListener
+    {
+	private JCheckBox stem_checkbox = new JCheckBox();
+	private JCheckBox casefold_checkbox = new JCheckBox();
+	private JCheckBox accentfold_checkbox = new JCheckBox();
+	private JCheckBox separate_cjk_checkbox =new JCheckBox();
+	
+	public StemmingControl()
+	{
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    setLayout(new BorderLayout(10,10));
+	    setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    
+	    stem_checkbox.setText(Dictionary.get("CDM.IndexingManager.Stem"));
+	    stem_checkbox.setToolTipText(Dictionary.get("CDM.IndexingManager.Stem_Tooltip"));
+	    stem_checkbox.setActionCommand(StaticStrings.STEM_OPTION_STR);
+	    stem_checkbox.setComponentOrientation(Dictionary.getOrientation());
+            
+	    casefold_checkbox.setText(Dictionary.get("CDM.IndexingManager.Casefold"));
+	    casefold_checkbox.setToolTipText(Dictionary.get("CDM.IndexingManager.Casefold_Tooltip"));
+	    casefold_checkbox.setActionCommand(StaticStrings.CASEFOLD_OPTION_STR);
+	    casefold_checkbox.setComponentOrientation(Dictionary.getOrientation());
+            
+	    accentfold_checkbox.setText(Dictionary.get("CDM.IndexingManager.Accent_fold"));
+	    accentfold_checkbox.setToolTipText(Dictionary.get("CDM.IndexingManager.Accent_fold_Tooltip"));
+	    accentfold_checkbox.setActionCommand(StaticStrings.ACCENTFOLD_OPTION_STR);
+	    // Accent-folding is currently not available for MG
+	    // accentfold_checkbox.setEnabled(!build_type.equals(BuildTypeManager.BUILD_TYPE_MG));
+            accentfold_checkbox.setComponentOrientation(Dictionary.getOrientation());
+            
+	    separate_cjk_checkbox.setText(Dictionary.get("CDM.IndexingManager.Separate_cjk"));
+	    separate_cjk_checkbox.setToolTipText(Dictionary.get("CDM.IndexingManager.Separate_cjk_Tooltip"));
+	    separate_cjk_checkbox.setActionCommand(StaticStrings.SEPARATE_CJK_OPTION_STR);
+            separate_cjk_checkbox.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel checkbox_panel = new JPanel();
+	    checkbox_panel.setLayout(new GridLayout(2, 1));
+	    checkbox_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel stem_panel = new JPanel();
+	    stem_panel.setLayout(new GridLayout(1,3));
+	    stem_panel.add(stem_checkbox);
+	    stem_panel.add(casefold_checkbox);
+	    stem_panel.add(accentfold_checkbox);
+	    stem_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel other_panel = new JPanel();
+	    other_panel.setLayout(new GridLayout(1,1));
+	    other_panel.add(separate_cjk_checkbox);
+            other_panel.setComponentOrientation(Dictionary.getOrientation());                    
+            
+	    checkbox_panel.add(stem_panel);
+	    checkbox_panel.add(other_panel);
+            
+            JLabel tmp = new JLabel(Dictionary.get("CDM.IndexingManager.Options"));
+            tmp.setComponentOrientation(Dictionary.getOrientation());
+	    add(tmp, BorderLayout.LINE_START);
+	    add(checkbox_panel, BorderLayout.CENTER);
+
+	    // set the defaults
+	    if (build_type.equals(BuildTypeManager.BUILD_TYPE_LUCENE)) {
+		stem_checkbox.setSelected(false);
+		stem_checkbox.setEnabled(false);
+		casefold_checkbox.setSelected(true);
+		casefold_checkbox.setEnabled(false);
+		accentfold_checkbox.setSelected(true);
+		accentfold_checkbox.setEnabled(false);
+		if (options_model.getOption(StaticStrings.SEPARATE_CJK_OPTION_STR) != null) {
+		    separate_cjk_checkbox.setSelected(true);
+		}
+	    } else {
+		if (build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+		    accentfold_checkbox.setEnabled(false);
+		}
+		
+		// set up the checked/unchecked boxes
+		    // we'll assume that the config file has got it correct
+		// no more setting all to on if nothing in config file
+		if (options_model.getSize()!=0) {
+		    if (options_model.getOption(StaticStrings.STEM_OPTION_STR)!=null) {
+			stem_checkbox.setSelected(true);
+		    }
+		    if (options_model.getOption(StaticStrings.CASEFOLD_OPTION_STR)!=null) {
+			casefold_checkbox.setSelected(true);
+		    }
+		    if (options_model.getOption(StaticStrings.ACCENTFOLD_OPTION_STR)!=null) {
+			accentfold_checkbox.setSelected(true);
+		    }
+		    if (options_model.getOption(StaticStrings.SEPARATE_CJK_OPTION_STR) != null) {
+			separate_cjk_checkbox.setSelected(true);
+		    }
+		    
+		}
+	    }
+	
+	    CheckBoxListener cbl = new CheckBoxListener();
+	    stem_checkbox.addActionListener(cbl);
+	    casefold_checkbox.addActionListener(cbl);
+ 	    accentfold_checkbox.addActionListener(cbl);
+	    separate_cjk_checkbox.addActionListener(cbl);
+	    
+	    // changing stem indexes changes build settings
+	    stem_checkbox.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    casefold_checkbox.addActionListener(CollectionDesignManager.buildcol_change_listener);
+ 	    accentfold_checkbox.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    separate_cjk_checkbox.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    
+	}
+	
+	public void buildTypeChanged(String new_build_type) {
+	    // accentfold only enabled for mgpp
+	    if (new_build_type.equals(BuildTypeManager.BUILD_TYPE_MGPP)) {
+		accentfold_checkbox.setEnabled(true);
+		accentfold_checkbox.setSelected(true);
+		options_model.addOption(StaticStrings.ACCENTFOLD_OPTION_STR);
+		if (build_type.equals(BuildTypeManager.BUILD_TYPE_LUCENE)) {
+		    // was lucene
+		    stem_checkbox.setEnabled(true);
+		    stem_checkbox.setSelected(true);
+		    options_model.addOption(StaticStrings.STEM_OPTION_STR);
+		    casefold_checkbox.setEnabled(true);
+		    casefold_checkbox.setSelected(true);
+		    options_model.addOption(StaticStrings.CASEFOLD_OPTION_STR);
+		} // else, was mg, stem and case remain the same
+		    
+	    }
+	    if (new_build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+		accentfold_checkbox.setSelected(false);
+		accentfold_checkbox.setEnabled(false);
+		options_model.removeOption(StaticStrings.ACCENTFOLD_OPTION_STR);
+	
+		if (build_type.equals(BuildTypeManager.BUILD_TYPE_LUCENE)) {
+		    // was lucene
+		    stem_checkbox.setEnabled(true);
+		    stem_checkbox.setSelected(true);
+		    options_model.addOption(StaticStrings.STEM_OPTION_STR);
+		    casefold_checkbox.setEnabled(true);
+		    casefold_checkbox.setSelected(true);
+		    options_model.addOption(StaticStrings.CASEFOLD_OPTION_STR);
+		} // else, was mgpp, stem and case remain the same
+	   
+	    }
+	    if (new_build_type.equals(BuildTypeManager.BUILD_TYPE_LUCENE)) {
+		accentfold_checkbox.setSelected(true);
+		accentfold_checkbox.setEnabled(false);
+		options_model.removeOption(StaticStrings.ACCENTFOLD_OPTION_STR);
+		stem_checkbox.setSelected(false);
+		stem_checkbox.setEnabled(false);
+		options_model.removeOption(StaticStrings.STEM_OPTION_STR);
+		casefold_checkbox.setSelected(true);
+		casefold_checkbox.setEnabled(false);
+		options_model.removeOption(StaticStrings.CASEFOLD_OPTION_STR);
+	
+	    }
+	}
+
+	private class CheckBoxListener
+	    implements ActionListener {
+	    public void actionPerformed(ActionEvent event) {
+		if (!((JCheckBox)event.getSource()).isSelected()) {
+		    // deselecting
+		    options_model.removeOption(event.getActionCommand());
+		}
+		else {
+		    // selecting a new one
+		    options_model.addOption(event.getActionCommand());
+		}
+	    }
+	}
+    }
+
+
+
+    /** This class creates a set of controls for editing the levels. */
+    private class LevelControl
+	extends JPanel 
+	implements BuildTypeManager.BuildTypeListener
+    {
+	
+	JCheckBox document_checkbox = null;
+	JCheckBox section_checkbox = null;
+	JRadioButton document_default_radio = null;
+	JRadioButton section_default_radio = null;
+	
+	public LevelControl() {
+	    this.setComponentOrientation(Dictionary.getOrientation());
+	    JPanel checkbox_panel = new JPanel();
+            checkbox_panel.setComponentOrientation(Dictionary.getOrientation());
+	    JPanel default_panel = new JPanel();
+            default_panel.setComponentOrientation(Dictionary.getOrientation());
+	    document_checkbox = new JCheckBox();
+            document_checkbox.setComponentOrientation(Dictionary.getOrientation());
+	    document_checkbox.setText(Dictionary.get("CDM.LevelManager.Document"));
+	    document_checkbox.setActionCommand(StaticStrings.DOCUMENT_STR);
+	    document_default_radio = new JRadioButton();
+	    document_default_radio.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    document_default_radio.setOpaque(false);
+	    document_default_radio.setActionCommand(StaticStrings.DOCUMENT_STR);
+	    document_default_radio.setIcon(JarTools.getImage("radio_unselected.gif"));
+	    document_default_radio.setSelectedIcon(JarTools.getImage("radio_selected.gif"));
+            document_default_radio.setComponentOrientation(Dictionary.getOrientation());
+            
+	    section_checkbox = new JCheckBox();
+	    section_checkbox.setText(Dictionary.get("CDM.LevelManager.Section"));
+	    section_checkbox.setActionCommand(StaticStrings.SECTION_STR);
+            section_checkbox.setComponentOrientation(Dictionary.getOrientation());
+            
+	    section_default_radio = new JRadioButton();
+	    section_default_radio.setOpaque(true);
+	    section_default_radio.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    section_default_radio.setOpaque(false);
+	    section_default_radio.setIcon(JarTools.getImage("radio_unselected.gif"));
+	    section_default_radio.setSelectedIcon(JarTools.getImage("radio_selected.gif"));
+            section_default_radio.setComponentOrientation(Dictionary.getOrientation());
+            
+	    section_default_radio.setActionCommand(StaticStrings.SECTION_STR);
+	    ButtonGroup default_group = new ButtonGroup();
+	    default_group.add(document_default_radio);
+	    default_group.add(section_default_radio);
+
+	 
+
+	    if (level_model.getSize()==0) {
+		// no levels yet, set document
+		document_checkbox.setSelected(true);
+		addLevel(StaticStrings.DOCUMENT_STR);
+	    } else {
+		if (getLevel(StaticStrings.DOCUMENT_STR)!= null) {
+		    document_checkbox.setSelected(true);
+		}
+		if (getLevel(StaticStrings.SECTION_STR) != null) {
+		    section_checkbox.setSelected(true);
+		}
+	    }
+
+	    if (default_level.isAssigned()) {
+		// set the default based on specified default
+		if (default_level.getValue().equals(StaticStrings.DOCUMENT_STR) && document_checkbox.isSelected()) {
+		    document_default_radio.setSelected(true);
+		}
+		else if (default_level.getValue().equals(StaticStrings.SECTION_STR) && section_checkbox.isSelected()) {
+		    section_default_radio.setSelected(true);
+		} else {
+    		    default_level.setAssigned(false);
+		}
+	    }
+	    // if have no valid specified default
+	    if (!default_level.isAssigned()) {
+		String default_l = StaticStrings.DOCUMENT_STR;
+		if (document_checkbox.isSelected()) {
+		    document_default_radio.setSelected(true);
+		    default_l = StaticStrings.DOCUMENT_STR;
+		} else if (section_checkbox.isSelected()) {
+		    section_default_radio.setSelected(true);
+		    default_l = StaticStrings.SECTION_STR;
+		}
+		setDefaultLevel(default_l);
+	    }
+	    
+	    validateControls();
+	    JLabel checkbox_label = new JLabel(Dictionary.get("CDM.LevelManager.Level_Title"));
+	    checkbox_panel.setLayout(new GridLayout(3,1));
+	    checkbox_panel.setToolTipText(Dictionary.get("CDM.IndexManager.Level_Tooltip"));
+	    checkbox_panel.add(checkbox_label);
+	    checkbox_panel.add(document_checkbox);
+	    checkbox_panel.add(section_checkbox);
+            checkbox_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JLabel default_label = new JLabel(Dictionary.get("CDM.LevelManager.Default"));
+	    default_panel.setLayout(new GridLayout(3,1));
+	    default_panel.setToolTipText(Dictionary.get("CDM.LevelManager.Default_Tooltip"));
+	    default_panel.add(default_label);
+	    default_panel.add(document_default_radio);
+	    default_panel.add(section_default_radio);
+            default_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	    CheckBoxListener cbl = new CheckBoxListener();
+	    document_checkbox.addActionListener(cbl);
+	    section_checkbox.addActionListener(cbl);
+
+	    // changing selected levels changes build settings
+	    document_checkbox.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    section_checkbox.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    
+	    RadioListener rl = new RadioListener();
+	    document_default_radio.addActionListener(rl);
+	    section_default_radio.addActionListener(rl);
+	    // changing the default doesn't need a rebuild
+
+	    setLayout(new BorderLayout(10,10));
+	    setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    add(checkbox_panel, BorderLayout.LINE_START);
+	    add(default_panel, BorderLayout.CENTER);
+	} // LevelControl
+	
+	public void buildTypeChanged(String new_build_type) {};
+	public void gainFocus() {
+
+	}
+
+	public void loseFocus() {
+
+	}
+
+	public void destroy() {
+	}
+
+	private class RadioListener
+	    implements ActionListener {
+	    public void actionPerformed(ActionEvent event) {
+		String new_level = event.getActionCommand();
+		setDefaultLevel(new_level);
+	    }
+	}
+	private class CheckBoxListener
+	    implements ActionListener {
+	    
+	    public void actionPerformed(ActionEvent event) {
+		if (!((JCheckBox)event.getSource()).isSelected()) {
+		    // deselecting
+		    if (noLevelsSelected()) {
+			// can't do this
+			((JCheckBox)event.getSource()).setSelected(true);
+		    } else {
+			// remove the level
+			removeLevel(event.getActionCommand());
+		    }
+		} else {
+		    // selecting a new one
+		    addLevel(event.getActionCommand());
+		}
+		validateControls();
+	    }
+	}
+	    
+	private boolean noLevelsSelected() {
+	    if (!document_checkbox.isSelected() && !section_checkbox.isSelected()) {
+		return true;
+
+	    }
+	    return false;
+	}
+
+	private void validateControls() {
+            
+	    boolean document_enabled = document_checkbox.isSelected();
+	    document_default_radio.setEnabled(document_enabled);
+	    
+	    boolean section_enabled = section_checkbox.isSelected();
+	    section_default_radio.setEnabled(section_enabled);
+
+	    // have we disabled our default??
+	    if (!document_enabled && document_default_radio.isSelected()) {
+		section_default_radio.setSelected(true);
+		setDefaultLevel(StaticStrings.SECTION_STR);
+               
+	    } else if (!section_enabled && section_default_radio.isSelected()) {
+		document_default_radio.setSelected(true);
+		setDefaultLevel(StaticStrings.DOCUMENT_STR);
+	    }
+
+           
+	}
+
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexingManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexingManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/IndexingManager.java	(revision 31635)
@@ -0,0 +1,175 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.GComboBox;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.util.CheckList;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.StaticStrings;
+
+
+/** This class is responsible for coordinating the IndexManager and IndexOptionManager (controls levels, stem options etc) and BuildTypeManager into one panel  
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class IndexingManager {
+
+    private IndexOptionManager option_manager = null;
+    private IndexManager index_manager = null;
+    private BuildTypeManager build_type_manager = null;
+
+
+    private String build_type = null;
+
+    private Control controls = null;
+
+
+    public IndexingManager()
+    {
+	build_type_manager = new BuildTypeManager();
+	build_type = build_type_manager.getBuildType();
+	option_manager = new IndexOptionManager(build_type);
+	if (build_type.equals(BuildTypeManager.BUILD_TYPE_MG)) {
+	    index_manager = new IndexManager(CollectionDesignManager.collect_config.getMGIndexes(), build_type);
+	} else {
+	    index_manager = new IndexManager(CollectionDesignManager.collect_config.getMGPPIndexes(), build_type);
+	}
+    }
+
+    public boolean isMGPP() {
+	return build_type_manager.isMGPP();
+    }
+
+    public boolean isLucene() {
+	return build_type_manager.isLucene();
+    }
+
+    public boolean isMG() {
+	return build_type_manager.isMG();
+    }
+
+    public int getNumLevels() {
+	return option_manager.getNumLevels();
+    }
+
+    public int getNumIndexes() {
+	return index_manager.getSize();
+    }
+
+    public ArrayList getIndexes() {
+	return index_manager.getIndexes();
+    }
+
+    public ArrayList getLevels() {
+      if (!isMG()) {
+	    return option_manager.getLevels();
+	}
+	return null;
+    }
+
+    public Control getControls() {
+	if (controls == null) {
+	    controls = new IndexingControl();
+	}
+	return controls;
+    }
+
+    public void destroy() {
+
+    }
+
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+
+    }
+
+
+    private class IndexingControl
+	extends JPanel
+	implements Control, BuildTypeManager.BuildTypeListener
+    {
+	JPanel main_index_pane = null;
+	JPanel index_options_panel = null;
+
+	public IndexingControl() {
+	    super();
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    // Creation
+	    JPanel header_pane = new DesignPaneHeader("CDM.GUI.Indexes", "searchindexes");	   
+	    
+	    JPanel build_type_panel = (JPanel)build_type_manager.getControls();
+	    JPanel index_panel = (JPanel)index_manager.getControls();
+	    index_options_panel = (JPanel)option_manager.getControls();
+                       
+            
+	    main_index_pane = new JPanel();
+	    main_index_pane.setLayout(new BorderLayout());
+	    main_index_pane.add(build_type_panel, BorderLayout.NORTH);
+	    main_index_pane.add(index_panel, BorderLayout.CENTER);
+	    main_index_pane.add(index_options_panel, BorderLayout.SOUTH);
+	    main_index_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(header_pane, BorderLayout.NORTH);
+	    add(main_index_pane, BorderLayout.CENTER);
+
+	    build_type_manager.addBuildTypeListener(this);
+	    build_type_manager.addBuildTypeListener(index_manager);
+	    build_type_manager.addBuildTypeListener(option_manager);
+	}
+
+	public void loseFocus() {}
+	public void gainFocus() {}
+	public void destroy() {}
+
+	public void buildTypeChanged(String new_build_type)
+	{
+	    if (build_type.equals(new_build_type)) {
+		// shouldn't happen
+		return;
+	    }
+ 	    build_type = new_build_type;
+	}
+
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Language.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Language.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Language.java	(revision 31635)
@@ -0,0 +1,173 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.util.ArrayList;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.*;
+
+
+/** A pretty unexciting extension of a two character string, in that it has a field which details if its the default language.
+* @author John Thompson, Greenstone Digital Library, University of Waikato
+* @version 2.1
+*/
+public class Language
+    implements Comparable, DOMProxyListEntry {
+    /** The Element this language entry is based upon. */
+    private Element element = null;
+    /** A comma separated list of two character codes */
+    private String code = null;
+    /** A comma separated list of language names */
+    private String name = null;
+ 
+    public Language() {
+    }
+
+    public Language(Element element) {
+	this.element = element;
+    }
+
+    /** Constructor for a brand new language. */
+    public Language(String code) {
+	this.code = code;
+	// Create the new element
+	element = CollectionConfiguration.createElement(StaticStrings.LANGUAGE_ELEMENT);
+	element.setAttribute(StaticStrings.NAME_ATTRIBUTE, code);
+	element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+    }
+
+    /** Constructor which takes an ArrayList of codes */
+    public Language(ArrayList codes) {
+	StringBuffer code_str = new StringBuffer();
+	boolean first = true;
+	for (int i=0; i<codes.size(); i++) {
+	    if (!first) {
+		code_str.append(",");
+	    } else {
+		first = false;
+	    }
+	    code_str.append(codes.get(i));
+	}
+	this.code = code_str.toString();
+	element = CollectionConfiguration.createElement(StaticStrings.LANGUAGE_ELEMENT);
+	element.setAttribute(StaticStrings.NAME_ATTRIBUTE, code);
+	element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+	
+    }
+
+    /** Method to compare two languages for ordering purposes.
+     * @param object the other language as an Object
+     * @return an int which indicates the order between this language and the given one: < 0, 0 or > 0 for before, equal to or after respectively
+     */
+    public int compareTo(Object object) {
+	return toString().compareTo(object.toString());
+    }
+
+    public DOMProxyListEntry create(Element element) {
+	return new Language(element);
+    }
+
+    /** Method to test for the equality of two languages.
+     * @param object The other language as an <strong>Object</strong>.
+     * @return <i>true</i> if the languages are equal, <i>false</i> otherwise.
+     */
+    public boolean equals(Object object) {
+	// two langs are equal if their codes are equal
+	if (object instanceof Language) {
+	    return getCode().equals(((Language)object).getCode());
+	} else if (object instanceof String) {
+	    return getCode().equals((String)object);
+	}
+	return false;
+    }
+
+    /** Method to retrieve the code of this language.
+     * @return A <strong>String</strong> representing the two letter code.
+     */
+    public String getCode() {
+	if(code == null && element != null) {
+	    code = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+	}
+	return code;
+    }
+
+    public Element getElement() {
+	return element;
+    }
+
+    public String getName() {
+	if(name == null) {
+	    String code_str = getCode();
+	    String [] codes = code_str.split(",");
+	    StringBuffer name_str = new StringBuffer();
+	    boolean first = true;
+	    for (int i=0; i<codes.length; i++) {
+		if (!first) {
+		    name_str.append(",");
+		} else {
+		    first = false;
+		}
+		name_str.append(CollectionDesignManager.language_manager.getLanguageName(codes[i]));
+	    }
+	    name = name_str.toString();
+	}
+	return name;
+    }
+
+    public boolean isAssigned() {
+	return (element != null && element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.TRUE_STR));
+    }
+
+    public void setAssigned(boolean value) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (value ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	}
+    }
+
+    public void setCode(String new_code) {
+	code = new_code;
+	// Set element
+	if(element != null) {
+	    element.setAttribute(StaticStrings.NAME_ATTRIBUTE, new_code);
+	}
+	// Reset name
+	name = null;
+    }
+
+    public void setElement(Element new_element) {
+	element = new_element;
+	code = null;
+	name = null;
+    }
+
+    /** Method to display the language code.
+     * @return A <strong>String</strong> representing the language code.
+     */
+    public String toString() {
+	return getName();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/LanguageListCellRenderer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/LanguageListCellRenderer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/LanguageListCellRenderer.java	(revision 31635)
@@ -0,0 +1,43 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+/**************************************************************************************
+ * Written:      11/07/03
+ * Revised:      
+ **************************************************************************************/
+import java.awt.*;
+import javax.swing.*;
+
+/** Provides a renderer which transforms a language code (such as en or fr) into its appropriate name (English and French respectively) before generating the rubberstamping component. */
+public class LanguageListCellRenderer
+    extends DefaultListCellRenderer {
+    
+    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+	return super.getListCellRendererComponent(list, CollectionDesignManager.language_manager.getLanguageName((String)value), index, isSelected, cellHasFocus);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/LanguageManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/LanguageManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/LanguageManager.java	(revision 31635)
@@ -0,0 +1,765 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.util.CheckList;
+import org.greenstone.gatherer.util.CheckListEntry;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+/** This class manages the language commands, remembering both a list of languages to build indexes in, plus the default language.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class LanguageManager
+    extends DOMProxyListModel {
+
+    static public Document LANGUAGES_DOCUMENT = XMLTools.parseXMLFile("xml/languages.xml", true);
+
+    static final private Dimension COMPONENT_SIZE = new Dimension(125,25);
+
+    /** The visual controls for this manager. */
+    private Control controls = null;
+    /** A reference to this class as a model, for the inner controls class. */
+    private DOMProxyListModel model = null;
+    /** A hashtable of code->name mappings of known languages. */
+    private LinkedHashMap known_languages = null;
+    /** The default language object. */
+    private Language default_language = null;
+    /** The language metadata element - specifies which metadata the 
+	language should be read from to determine the partitons */
+    private Element language_metadata = null;
+   /** Constructor. */
+    public LanguageManager(Element languages_element) {
+	super(languages_element, StaticStrings.LANGUAGE_ELEMENT, new Language());
+	
+	DebugStream.println("LanguageManager: " + getSize() + " languages parsed.");
+
+	this.model = this;
+	// Retrieve the default language
+	NodeList default_language_elements = CollectionConfiguration.getElementsByTagName(StaticStrings.LANGUAGE_DEFAULT_ELEMENT);
+	if(default_language_elements.getLength() > 0) {
+	    default_language = new Language((Element)default_language_elements.item(0));
+	}
+
+	// Retrieve the language metadata
+	language_metadata = CollectionDesignManager.collect_config.getLanguageMetadata();
+	// Load a series of code->language mappings into known_languages, by reading from the 'languages.xml' file, which is essentially a subset of the ISO 639 Standard.
+	known_languages = new LinkedHashMap();
+	NodeList language_elements = LANGUAGES_DOCUMENT.getDocumentElement().getElementsByTagName(StaticStrings.LANGUAGE_ELEMENT);
+	for(int i = 0; i < language_elements.getLength(); i++) {
+	    Element language_element = (Element) language_elements.item(i);
+	    String code = language_element.getAttribute(StaticStrings.CODE_ATTRIBUTE);
+	    String name = language_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+	    known_languages.put(code.toLowerCase(), name);
+	    name = null;
+	    code = null;
+	    language_element = null;
+	}
+    }
+
+    /** Method to add a new language.
+     * @param language The <strong>Language</strong> to add.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     */
+    private void addLanguage(Language language) {
+	if(!contains(language)) {
+	    // need to add a pseudo metadata
+	    CollectionMeta metadatum = new CollectionMeta(StaticStrings.STOP_CHARACTER + language.getCode());
+	    metadatum.setValue(language.getName());
+	    CollectionDesignManager.collectionmeta_manager.addMetadatum(metadatum);
+	    add(getSize(), language);
+	}
+    }
+
+    public void destroy() {
+	if(controls != null) {
+	    controls.destroy();
+	    controls = null;
+	}
+	known_languages.clear();
+	known_languages = null;
+	default_language = null;
+    }
+
+    /** Method to retrieve the control for this manager.
+     * @return the Control for editing the language partitions
+     */
+    public Control getControls() {
+	if(controls == null) {
+	    // Build controls
+	    controls = new LanguageControl();
+	}
+	return controls;
+    }
+    
+    /** Method to retrieve a certain language object by its code.
+     * @param code The two letter code of a language, as a <strong>String</strong>.
+     * @return The <strong>Language</strong> that matches the given code, or <i>null</i> if no such language exists.
+     */
+    public Language getLanguage(String code) {
+	int size = getSize();
+	for(int i = 0; i < size; i++) {
+	    Language language = (Language) getElementAt(i);
+	    if(language.getCode().equals(code)) {
+		return language;
+	    }
+	}
+	return null;
+    }
+
+    public ArrayList getLanguages() {
+	return children();
+    }
+
+    /** Method to return a list of the known language codes.
+     * @return an ArrayList containing the series of known language codes as per the languages.dat file
+     */
+    public ArrayList getLanguageCodes() {
+	return new ArrayList(known_languages.keySet());
+    }
+
+    public String getLanguageName(String code) {
+	return (String) known_languages.get(code);
+    }
+    
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+	
+    }
+
+   private int moveLanguage(Language lang, boolean move_up)
+    {
+	// Determine the current position of the language
+	int position = indexOf(lang);
+	int new_position;
+
+	// Attempt to move the language up
+	if (move_up) {
+	    // Check it's not already at the top
+	    if (position == 0) {
+		return position;
+	    }
+
+	    // This automatically removes the language first, as an Element can only exist once in a particular document
+	    new_position = position - 1;
+	    addBefore(lang, (Language) getElementAt(new_position));
+	}
+
+	// Attempt to move the language down
+	else {
+	    // Check it's not already at the bottom
+	    if (position == (getSize()) - 1) {
+		return position;
+	    }
+
+	    // This automatically removes the language first, as an Element can only exist once in a particular document
+	    new_position = position + 1;
+	    addAfter(lang, (Language) getElementAt(new_position));
+	}
+
+	return new_position;
+    }
+
+    /** Method to remove a certain language.
+     * @param language The <strong>Language</strong> to remove.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     */
+    private void removeLanguage(Language language) {
+	remove(language);
+	// Remove any collection metadata for this language
+	CollectionDesignManager.collectionmeta_manager.removeMetadata(StaticStrings.STOP_CHARACTER + language.getCode());
+	if(default_language != null && default_language.equals(language)) {
+	    setDefault(null);
+	}
+    }
+
+    private void replaceLanguage(Language old_language, Language new_language) {
+	// Remove old lang collection meta
+	CollectionDesignManager.collectionmeta_manager.removeMetadata(StaticStrings.STOP_CHARACTER + old_language.getCode());
+	// Add new one
+	CollectionMeta metadatum = new CollectionMeta(StaticStrings.STOP_CHARACTER + new_language.getCode());
+	metadatum.setValue(new_language.getName());
+	CollectionDesignManager.collectionmeta_manager.addMetadatum(metadatum);
+	if(default_language != null && default_language.equals(old_language)) {
+	    setDefault(new_language);
+	}
+
+	// get the position of the old one
+	int position = indexOf(old_language);
+	remove(old_language);
+	add(position, new_language);
+    }
+
+
+    /** Method to set the default language.
+     * @param language The <strong>Language</strong> to use as a default, or <i>null</i> for no default.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     */
+    public void setDefault(Language language) {
+	if(language != null) {
+	    if(default_language == null) {
+		// Create the default index element, and place immediately after indexes element.
+		Element default_language_element = root.getOwnerDocument().createElement(StaticStrings.LANGUAGE_DEFAULT_ELEMENT);
+		default_language = new Language(default_language_element);
+		Node target_node = CollectionConfiguration.findInsertionPoint(default_language_element);
+		if(target_node != null) {
+		    root.getOwnerDocument().getDocumentElement().insertBefore(default_language_element, target_node);
+		}
+		else {
+		    root.getOwnerDocument().getDocumentElement().appendChild(default_language_element);
+		}
+	    }
+	    default_language.setAssigned(true);
+	    default_language.setCode(language.getCode());
+	}
+	else {
+	    if(default_language != null) {
+		default_language.setAssigned(false);
+	    }
+	}
+    }
+
+
+    /** This class represents the visual component of the Language Manager. */
+    private class LanguageControl
+	extends JPanel 
+	implements Control {
+	/** The list of available languages */
+	private CheckList language_list = null;
+	/** The button to add a new language support. */
+	private JButton add_button = null;
+	/** The button to replace a language support. */
+	private JButton replace_button = null;
+	/** The button to remove a supported language. */
+	private JButton remove_button = null;
+	/** button to move a language up in the list */
+	private JButton move_down_button;
+	/** button to move a language down in the list */
+	private JButton move_up_button;
+	/** button to select all the languages in the list */
+	private JButton select_all_button;
+	/** button to clear the current selection in the list */
+	private JButton select_none_button; 
+	/** The button to set the current language as the default one. */
+	private JButton set_default_button = null;
+	/** A list of currently supported languages. */
+	private JList selected_languages_list = null;
+	/** A list of metadata elements that may hold the language metadata */
+	private JComboBox language_metadata_combo = null;
+	
+	/** Constructor.
+	 * @see org.greenstone.gatherer.cdm.LanguageManager.LanguageControl.AddListener
+	 * @see org.greenstone.gatherer.cdm.LanguageManager.LanguageControl.ClearDefaultListener
+	 * @see org.greenstone.gatherer.cdm.LanguageManager.LanguageControl.ListListener
+	 * @see org.greenstone.gatherer.cdm.LanguageManager.LanguageControl.RemoveListener
+	 * @see org.greenstone.gatherer.cdm.LanguageManager.LanguageControl.SelectorListener
+	 * @see org.greenstone.gatherer.cdm.LanguageManager.LanguageControl.SetDefaultListener
+	 */
+	public LanguageControl() {
+	    super();
+	    // Creation.
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    JPanel center_panel = new JPanel();
+            center_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JLabel selected_languages_list_label = new JLabel(Dictionary.get("CDM.LanguageManager.Assigned_Languages"));
+	    selected_languages_list = new JList(model);
+	    selected_languages_list.setCellRenderer(new MyLanguageListCellRenderer());
+	    selected_languages_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+	    selected_languages_list.setVisibleRowCount(5);
+	    selected_languages_list.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel control_panel = new JPanel();
+	    control_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel labels_pane = new JPanel();	    
+	    labels_pane.setComponentOrientation(Dictionary.getOrientation());
+            JLabel selector_label = new JLabel(Dictionary.get("CDM.LanguageManager.Selector"));
+            selector_label.setComponentOrientation(Dictionary.getOrientation());
+	    select_all_button = new GLIButton(Dictionary.get("CDM.IndexManager.Select_All"), Dictionary.get("CDM.IndexManager.Select_All_Tooltip"));
+	    select_none_button = new GLIButton(Dictionary.get("CDM.IndexManager.Select_None"), Dictionary.get("CDM.IndexManager.Select_None_Tooltip"));
+	    
+	    language_metadata_combo = new JComboBox(MetadataSetManager.getEveryMetadataSetElement().toArray());
+	    language_metadata_combo.setOpaque(false);
+	    language_metadata_combo.setToolTipText(Dictionary.get("CDM.LanguageManager.LanguageMetadata_Tooltip"));
+            language_metadata_combo.setComponentOrientation(Dictionary.getOrientation());
+            
+            JLabel language_metadata_label = new JLabel(Dictionary.get("CDM.LanguageManager.LanguageMetadata"));
+            language_metadata_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    String current_value = "ex.Language";
+	    if (language_metadata.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.TRUE_STR) && !language_metadata.getAttribute(StaticStrings.NAME_ATTRIBUTE).equals("")) {
+		current_value = language_metadata.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+	    }
+	    ArgumentControl.selectValue(language_metadata_combo, current_value);
+	    language_list = new CheckList(false);
+	    language_list.setListData(getLanguageCodes());
+	    language_list.setToolTipText(Dictionary.get("CDM.LanguageManager.Selector_Tooltip"));
+	    language_list.setCellRenderer(new LanguageCheckListCellRenderer());
+	    
+	    select_all_button.setEnabled(isSelectAllEnabled());
+	    select_none_button.setEnabled(isSelectAllEnabled());
+	    	    
+	    JPanel movement_pane = new JPanel();
+            movement_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+            move_up_button = new GLIButton(Dictionary.get("CDM.Move.Move_Up"), JarTools.getImage("arrow-up.gif"), Dictionary.get("CDM.Move.Move_Up_Tooltip"));
+	    move_up_button.setEnabled(false);
+	    
+	    move_down_button = new GLIButton(Dictionary.get("CDM.Move.Move_Down"), JarTools.getImage("arrow-down.gif"), Dictionary.get("CDM.Move.Move_Down_Tooltip"));
+	    move_down_button.setEnabled(false);
+	    
+	    set_default_button = new GLIButton(Dictionary.get("CDM.LanguageManager.Set_Default"), Dictionary.get("CDM.LanguageManager.Set_Default_Tooltip"));
+	    set_default_button.setEnabled(false);
+	    
+	    JPanel button_panel = new JPanel();
+            button_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    add_button = new GLIButton(Dictionary.get("CDM.SubcollectionIndexManager.Add_Subindex"), Dictionary.get("CDM.LanguageManager.Add_Tooltip"));
+	    add_button.setEnabled(false);
+
+	    replace_button = new GLIButton(Dictionary.get("CDM.SubcollectionIndexManager.Replace_Subindex"), Dictionary.get("CDM.LanguageManager.Replace_Tooltip"));
+	    replace_button.setEnabled(false);
+	    
+	    remove_button = new GLIButton(Dictionary.get("CDM.SubcollectionIndexManager.Remove_Subindex"), Dictionary.get("CDM.LanguageManager.Remove_Tooltip"));
+	    remove_button.setEnabled(false);
+	    
+	    // Set up and connect listeners.
+	    add_button.addActionListener(new AddListener());
+	    add_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    move_down_button.addActionListener(new MoveListener(false));
+	    move_down_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    move_up_button.addActionListener(new MoveListener(true));
+	    move_up_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    remove_button.addActionListener(new RemoveListener());
+	    remove_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    replace_button.addActionListener(new ReplaceListener());
+	    replace_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    select_all_button.addActionListener(new SelectAllListener());			
+	    select_all_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    select_none_button.addActionListener(new SelectNoneListener());
+	    select_none_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+
+	    language_list.addListSelectionListener(new LanguageListListener());
+	    language_metadata_combo.addActionListener(new ActionListener() {
+		    public void actionPerformed(ActionEvent event) {
+			String value = ((MetadataElement)language_metadata_combo.getSelectedItem()).getFullName();
+			language_metadata.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+			language_metadata.setAttribute(StaticStrings.NAME_ATTRIBUTE, value);
+		    }
+		});
+	    set_default_button.addActionListener(new SetDefaultListener());
+	    set_default_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    selected_languages_list.addListSelectionListener(new AssignedListListener());
+
+	    // Layout components
+	    button_panel.setLayout(new GridLayout(1,3));
+	    button_panel.add(add_button);
+	    button_panel.add(replace_button);
+	    button_panel.add(remove_button);
+
+	    JPanel metadata_panel = new JPanel();
+            metadata_panel.setComponentOrientation(Dictionary.getOrientation());
+	    metadata_panel.setLayout(new BorderLayout());
+	    JPanel inner_panel = new JPanel();
+            inner_panel.setComponentOrientation(Dictionary.getOrientation());
+	    inner_panel.setLayout(new BorderLayout(10,10));
+	    inner_panel.add(language_metadata_label, BorderLayout.LINE_START);
+	    inner_panel.add(language_metadata_combo, BorderLayout.CENTER);
+	    metadata_panel.add(new JPanel(), BorderLayout.CENTER);
+	    metadata_panel.add(inner_panel, BorderLayout.LINE_END);
+
+	    movement_pane.setBorder(BorderFactory.createEmptyBorder(0,2,0,0));
+	    movement_pane.setLayout(new GridLayout(3,1));
+	    movement_pane.add(move_up_button);
+	    movement_pane.add(move_down_button);
+	    movement_pane.add(set_default_button);
+
+	    control_panel.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    control_panel.setLayout(new BorderLayout());
+	    	    
+	    labels_pane.setLayout(new BorderLayout());
+	    labels_pane.setBorder(BorderFactory.createEmptyBorder(5, 5, 10, 5));
+	    
+	    // add the select all and select none buttons to the label panel			 
+	    GridBagLayout gridbag = new GridBagLayout();			
+	    GridBagConstraints c = new GridBagConstraints();
+	    labels_pane.setLayout(gridbag);
+
+	    c.fill = GridBagConstraints.BOTH;		
+	    c.weightx = 1.0;
+	    c.weighty = 1.0;
+	    c.gridx = 0;
+	    c.gridy = 0;
+	    
+	    gridbag.setConstraints(selector_label, c);
+	    labels_pane.add(selector_label);
+	    
+	    c.fill = GridBagConstraints.HORIZONTAL;
+	    c.weighty = 0.0;
+	    c.gridy = 1;
+	    
+	    gridbag.setConstraints(select_all_button, c);
+	    labels_pane.add(select_all_button);			
+	    
+	    c.gridy = 2;
+	    gridbag.setConstraints(select_none_button, c);
+	    labels_pane.add(select_none_button);	
+	    	    
+	    control_panel.add(labels_pane, BorderLayout.LINE_START);
+	    
+	    control_panel.add(new JScrollPane(language_list), BorderLayout.CENTER);
+	    control_panel.add(button_panel, BorderLayout.SOUTH);
+
+	    center_panel.setLayout(new BorderLayout());
+	    center_panel.add(selected_languages_list_label, BorderLayout.NORTH);
+	    center_panel.add(new JScrollPane(selected_languages_list), BorderLayout.CENTER);
+	    center_panel.add(movement_pane, BorderLayout.LINE_END);
+
+	    JPanel top_panel = new JPanel();
+            top_panel.setComponentOrientation(Dictionary.getOrientation());
+            top_panel.setLayout(new BorderLayout());
+	    top_panel.add(metadata_panel, BorderLayout.NORTH);
+	    top_panel.add(center_panel, BorderLayout.SOUTH);
+	    
+	    setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    setLayout(new BorderLayout());
+	    add(top_panel, BorderLayout.NORTH);
+	    add(control_panel, BorderLayout.CENTER);
+	}
+	
+	/** Destructor.	*/
+	public void destroy() {
+	}
+
+	public void gainFocus() {
+	}
+
+	public void loseFocus() {
+	}
+
+	
+	private void clearControls() {
+	    selected_languages_list.clearSelection();
+	    language_list.clearTicked();
+	    add_button.setEnabled(false);
+	    remove_button.setEnabled(false);
+	    replace_button.setEnabled(false);
+	    set_default_button.setEnabled(false);
+	    move_down_button.setEnabled(false);
+	    move_up_button.setEnabled(false);
+	    select_all_button.setEnabled(isSelectAllEnabled());			
+	    select_none_button.setEnabled(isSelectAllEnabled());
+
+	}
+	
+	private void updateControlsWithSelectedLanguage()
+	{
+	    Language selected_lang = (Language) selected_languages_list.getSelectedValue();
+	    if (selected_lang == null) {
+		clearControls();
+		return;
+	    }
+	    
+	    // Display the selected subcollection index's sources
+	    language_list.clearTicked();	
+	    // for multi-languages indexes, getCode returns null
+	    if(selected_lang.getCode()==null){	      	      
+	      return;
+	    }
+	    language_list.setTickedObjects(selected_lang.getCode().split(","));	    
+	}
+
+	private void validateButtons() {
+	    boolean add_enabled = false;
+	    boolean replace_enabled = false;
+
+	    if (!language_list.isNothingTicked()) {
+		// Create a dummy Langauge and see if its in the collection
+		ArrayList langs = language_list.getTicked();
+		StringBuffer code_str = new StringBuffer();
+		boolean first = true;
+		for (int i=0; i<langs.size(); i++) {
+		    if (!first) {
+			code_str.append(",");
+		    } else {
+			first = false;
+		    }
+		    code_str.append(langs.get(i));
+		}
+		String lang_str = code_str.toString();
+		
+		if (!model.contains(lang_str)) {
+		    add_enabled = true;
+		    if (!selected_languages_list.isSelectionEmpty()) {
+			replace_enabled = true;
+		    }
+		}
+
+	    }
+	    add_button.setEnabled(add_enabled);
+	    replace_button.setEnabled(replace_enabled);
+	    select_all_button.setEnabled(isSelectAllEnabled());
+	    select_none_button.setEnabled(isSelectAllEnabled());
+	}
+	
+	/** Listens for actions apon the 'add' button in the LanguageManager controls, and if detected calls the add method of the manager with a newly created language. */
+	private class AddListener
+	    implements ActionListener {
+	    /** Add a new language support.
+	     * @param event an ActionEvent
+	     * @see org.greenstone.gatherer.cdm.Language
+	     */
+	    public void actionPerformed(ActionEvent event) {
+		if (!language_list.isNothingTicked()) {
+		    addLanguage(new Language(language_list.getTicked()));
+		    clearControls();
+		}
+	    }
+	}
+
+	/** Listens for actions apon the 'remove' button in the LanguageManager controls, and if detected calls the remove method of the manager with the language selected for removal. */
+	private class RemoveListener
+	    implements ActionListener {
+	    /** Remove the currently selected language, if any.
+	     * @param event An <strong>ActionEvent</strong>.
+	     * @see org.greenstone.gatherer.cdm.Language
+	     */
+	    public void actionPerformed(ActionEvent event) {
+		Language delete_me = (Language)selected_languages_list.getSelectedValue();
+		if(delete_me != null) {
+		    removeLanguage(delete_me);
+		}
+	    }
+	}
+
+	private class ReplaceListener
+	    implements ActionListener {
+	    
+	    public void actionPerformed(ActionEvent event) {
+		if (selected_languages_list.isSelectionEmpty() || language_list.isNothingTicked()) {
+		    // This should never happen, but just in case...
+		    replace_button.setEnabled(false);
+		    return;
+		}
+		Language old_language = (Language) selected_languages_list.getSelectedValue();
+		Language new_language = new Language(language_list.getTicked());
+		replaceLanguage(old_language, new_language);
+		
+	    }
+	}
+	
+	private class LanguageListListener
+	    implements ListSelectionListener {
+
+	    public void valueChanged(ListSelectionEvent event) {
+		if (event.getValueIsAdjusting()) {
+		    return;
+		}
+		validateButtons();
+	    }
+	}
+	
+	private class SelectAllListener
+	implements ActionListener {
+		
+		public void actionPerformed(ActionEvent event){
+			if(select_all_button.isEnabled()){
+			  language_list.setAllTicked();
+			  validateButtons();
+			}
+		}
+	}
+	
+	private class SelectNoneListener
+	implements ActionListener {
+		
+		public void actionPerformed(ActionEvent event){
+			if(select_all_button.isEnabled()){
+			  language_list.clearTicked();
+			  validateButtons();
+			}
+		}
+	}
+	
+	/** Listens for actions apon the 'set default' button in the LanguageManager controls, and if detected calls the <i>setDefault()</i> method of the manager with the language selected for default. */
+	private class SetDefaultListener
+	    implements ActionListener {
+	    /** Set the default index to the one currently selected, if any.
+	     * @param event An <strong>ActionEvent</strong>.
+	     * @see org.greenstone.gatherer.cdm.Language
+	     */
+	    public void actionPerformed(ActionEvent event) {
+		Language selected_language = (Language) selected_languages_list.getSelectedValue();
+		if(selected_language != null) {
+		    setDefault(selected_language);
+		    // This should cause a repaint of just the desired row
+		    selected_languages_list.setSelectedValue(selected_language, true);
+		}
+		set_default_button.setEnabled(false);
+	    }
+	}
+
+	private class MoveListener
+	    implements ActionListener
+	{
+	    private boolean move_up;
+
+	    public MoveListener(boolean move_up)
+	    {
+		this.move_up = move_up;
+	    }
+
+	    public void actionPerformed(ActionEvent event)
+	    {
+		// Retrieve the selected language
+		Language language = (Language) selected_languages_list.getSelectedValue();
+		if (language != null) {
+		    int new_position = moveLanguage(language, move_up);
+		    // Ensure the language that moved is still selected
+		    selected_languages_list.setSelectedIndex(new_position);
+		}
+	    }
+	}
+
+
+	/** Listens for selections within the list on the LanguageManager controls, and if a change is detected enables, or disables, controls appropriately. */
+	private class AssignedListListener
+	    implements ListSelectionListener {
+	    /** Enable or disable controls depending on the current list selection.
+	     * @param event A <strong>ListSelectionEvent</strong>.
+	     */
+	    public void valueChanged(ListSelectionEvent event) {
+		if (event.getValueIsAdjusting()) {
+		    return;
+		}
+		if(selected_languages_list.isSelectionEmpty()) {
+		    clearControls();
+		    return;
+		}
+		
+		int i = selected_languages_list.getSelectedIndex();
+ 		int size = selected_languages_list.getModel().getSize();
+ 		
+ 		select_all_button.setEnabled(isSelectAllEnabled());
+		select_none_button.setEnabled(isSelectAllEnabled());
+		
+		Language selected_lang = (Language)selected_languages_list.getSelectedValue();
+		remove_button.setEnabled(true);
+		replace_button.setEnabled(false);
+		add_button.setEnabled(false);
+		set_default_button.setEnabled(default_language == null || !default_language.equals(selected_lang));
+		
+		if (i > 0) {
+ 		    move_up_button.setEnabled(true);
+ 		}
+ 		else {
+ 		    move_up_button.setEnabled(false);
+ 		}
+ 		if (i < size-1){
+ 		    move_down_button.setEnabled(true);
+ 		}
+ 		else {
+ 		    move_down_button.setEnabled(false);
+ 		}
+		updateControlsWithSelectedLanguage();
+	    }
+	}
+
+	private class MyLanguageListCellRenderer
+	    extends DefaultListCellRenderer
+	{
+	    /** Return a component that has been configured to display the specified value. */
+	    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+		JLabel component = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+		if (default_language != null && default_language.equals(value)) {
+		    component.setText(component.getText() + " [" + Dictionary.get("CDM.LanguageManager.Default_Language")+"]");
+		}
+		return component;
+	    }
+	}
+	
+	private boolean isSelectAllEnabled(){
+	  return language_list.getModel().getSize() > 0 ? true : false;		  	
+	}
+    }
+    
+    /** A custom list cell renderer for producing rows which contain clickable check boxes. */
+    private class LanguageCheckListCellRenderer 
+	implements ListCellRenderer
+    {
+	/** Return a component that has been configured to display the specified value. That component's paint method is then called to "render" the cell. If it is necessary to compute the dimensions of a list because the list cells do not have a fixed size, this method is called to generate a component on which getPreferredSize  can be invoked.
+	 * @param list The </strong>JList</strong> we're painting.
+	 * @param value The value returned by list.getModel().getElementAt(index), as an <strong>Object</strong>.
+	 * @param index The cells index as an <i>int</i>.
+	 * @param is_selected <i>true</i> if the specified cell was selected, <i>false</i> otherwise.
+	 * @param cell_has_focus <i>true</i> if and only if the specified cell has the focus.
+	 * @return A <strong>Component</strong> whose paint() method will render the specified value.
+	 */
+	public Component getListCellRendererComponent(JList list, Object value, int index, boolean is_selected, boolean cell_has_focus) {
+	    JCheckBox checkbox = (JCheckBox) value;
+	    checkbox.setBackground(list.getBackground());
+	    checkbox.setForeground(list.getForeground());
+	    checkbox.setBorderPainted(false);
+	    checkbox.setEnabled(list.isEnabled());
+	    checkbox.setFont(list.getFont());
+	    checkbox.setFocusPainted(false);
+	    checkbox.setBorder((is_selected) ? UIManager.getBorder("List.focusCellHighlightBorder") : new EmptyBorder(1, 1, 1, 1));
+	    
+	    String code = (String)((CheckListEntry)list.getModel().getElementAt(index)).getObject();
+	    checkbox.setText((String)known_languages.get(code));
+
+	    return checkbox;
+	}
+    }
+    
+    
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/MacrosManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/MacrosManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/MacrosManager.java	(revision 31635)
@@ -0,0 +1,301 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Katherine Don, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.undo.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+
+
+public class MacrosManager {
+
+    /** The controls used to modify the general options. */
+    private Control controls;
+    /** Constructor. */
+    public MacrosManager() {
+    }
+
+
+    /** Destructor. */
+    public void destroy() {
+	if (controls != null) {
+	    controls.destroy();
+	    controls = null;
+	}
+    }
+
+    public void loseFocus() {
+    }
+
+    public void gainFocus() {
+
+    }
+    /** This class is resposible for generating the controls for the editing of general options.
+     * @return the Control for editing the general options
+     */
+    public Control getControls() {
+	if (controls == null) {
+	    controls = new MacrosControl();
+	}
+	return controls;
+    }
+    
+
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+
+    }
+
+
+    private class MacrosControl
+	extends JPanel
+	implements Control, WindowFocusListener {
+
+	JTextArea macros_textarea = null;
+	JButton undo_button = null;
+	JButton redo_button = null;
+	private final UndoManager undo = new UndoManager();
+	private boolean macros_changed = false;
+	public MacrosControl() {
+	    super();
+            this.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel header_pane = new DesignPaneHeader("CDM.GUI.Macros", "collectionspecificmacros");	    
+	    JPanel main_pane = new JPanel();
+	    main_pane.setComponentOrientation(Dictionary.getOrientation());
+            main_pane.setLayout(new BorderLayout());
+	    macros_textarea = new JTextArea();
+            macros_textarea.setComponentOrientation(Dictionary.getOrientation());
+	    macros_textarea.setBackground(Configuration.getColor("coloring.editable_background", false));
+	    macros_textarea.setLineWrap(false);
+	    macros_textarea.setWrapStyleWord(false);
+	    macros_textarea.setToolTipText(Dictionary.get("CDM.MacrosManager.Editor_Tooltip"));
+	    readMacroFile(); // load in the original contents
+	    macros_textarea.getDocument().addDocumentListener(new EditorListener());
+
+	    // Listen for undo and redo events
+	    macros_textarea.getDocument().addUndoableEditListener(new UndoableEditListener() {
+		    public void undoableEditHappened(UndoableEditEvent evt) {
+			undo.addEdit(evt.getEdit());
+		    }
+		});
+	    macros_textarea.setCaretPosition(0);
+	    
+	    JPanel macros_pane = new JPanel();
+            macros_pane.setComponentOrientation(Dictionary.getOrientation());
+	    macros_pane.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));
+	    macros_pane.setLayout(new BorderLayout());
+	    macros_pane.add(new JScrollPane(macros_textarea), BorderLayout.CENTER);
+	    undo_button = new GLIButton(Dictionary.get("General.Undo"), Dictionary.get("General.Undo_Tooltip"));
+	    undo_button.setEnabled(false);
+	    undo_button.addActionListener(new UndoListener());
+
+	    redo_button = new GLIButton(Dictionary.get("General.Redo"), Dictionary.get("General.Redo_Tooltip"));
+	    redo_button.setEnabled(false);
+	    redo_button.addActionListener(new RedoListener());
+	    
+	    JPanel button_pane = new JPanel();
+            button_pane.setComponentOrientation(Dictionary.getOrientation());
+            button_pane.setLayout(new GridLayout(1,2));
+	    button_pane.add(undo_button);
+	    button_pane.add(redo_button);
+
+	    main_pane.add(macros_pane, BorderLayout.CENTER);
+	    main_pane.add(button_pane, BorderLayout.SOUTH);
+
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(header_pane, BorderLayout.NORTH);
+	    add(main_pane, BorderLayout.CENTER);
+	    Gatherer.g_man.addWindowFocusListener(this);
+	}
+
+	public void destroy() {
+	}
+
+	public void loseFocus() {
+	    if (macros_changed) {
+		writeMacroFile();
+		macros_changed = false;
+	    }
+	}
+
+	public void gainFocus() {
+	    macros_textarea.grabFocus();
+	}
+	
+	public void windowGainedFocus(WindowEvent e) {
+	}
+	
+	public void windowLostFocus(WindowEvent e) {
+	    if (macros_changed) {
+		writeMacroFile();
+		macros_changed = false;
+	    }	    
+	}
+
+	private void readMacroFile() {
+	    macros_textarea.setText("");
+	    File extra_dm = new File(CollectionManager.getLoadedCollectionMacrosDirectoryPath() + "extra.dm");
+	    
+	    if (extra_dm.exists()) {
+		try {
+		    BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(extra_dm), "UTF-8"));
+		
+		    String line;
+		    while ((line = br.readLine()) != null) {
+			macros_textarea.append(line+"\n");
+		    }
+
+		    br.close();
+		}
+		catch (Exception exception) {
+		    DebugStream.printStackTrace(exception);
+		}
+	    }
+	}
+	
+	private void writeMacroFile() {
+	    File extra_dm_file = new File(CollectionManager.getLoadedCollectionMacrosDirectoryPath() + "extra.dm");
+	    try {
+		if (!extra_dm_file.exists()) {
+		    File parent_dir = extra_dm_file.getParentFile();
+		    parent_dir.mkdirs();
+		    extra_dm_file.createNewFile();
+		}
+		
+		BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(extra_dm_file), "UTF-8"));
+		String text = macros_textarea.getText();
+		out.write(text, 0, text.length());
+		out.newLine();
+		out.flush();
+		out.close();
+
+		// If we're using a remote Greenstone server, upload the new extra.dm file
+		// This will return immediately, since we're on the event dispatch thread
+		// Users need to wait until the upload has finished before pressing Preview Collection!
+		if (Gatherer.isGsdlRemote) {
+		    Gatherer.remoteGreenstoneServer.uploadCollectionFile(CollectionManager.getLoadedCollectionName(), extra_dm_file);
+		}
+	    }
+	    catch (Exception exception) {
+		DebugStream.printStackTrace(exception);
+	    }
+	}
+
+	private class EditorListener
+	    implements DocumentListener {
+	    public void changedUpdate(DocumentEvent e) {
+		macros_changed = true;
+		undo_button.setEnabled(true);
+	    }
+          
+	    public void insertUpdate(DocumentEvent e) {
+		macros_changed = true;
+		undo_button.setEnabled(true);
+	    }
+          
+	    public void removeUpdate(DocumentEvent e) {
+		macros_changed = true;
+		undo_button.setEnabled(true);
+	    }
+
+	}
+
+	private class UndoListener
+	    implements ActionListener {
+	    
+	    public void actionPerformed(ActionEvent event) {
+		try {
+                    if (undo.canUndo()) {
+                        int pos = macros_textarea.getCaretPosition();
+                        undo.undo();
+			macros_textarea.setCaretPosition(pos-1);
+			macros_textarea.grabFocus();
+			redo_button.setEnabled(true); 
+			
+                    }
+                    if (!undo.canUndo()){
+			undo_button.setEnabled(false);
+		    }
+		    else{
+                      	undo_button.setEnabled(true); 
+		    }   
+                }
+		catch (Exception exception) {
+		    DebugStream.printStackTrace(exception);
+                }           
+	    }
+	}
+    
+
+	private class RedoListener
+	    implements ActionListener {
+	   
+	    public void actionPerformed(ActionEvent evt) {
+		try {
+		    if (undo.canRedo()) {
+			int pos = macros_textarea.getCaretPosition();
+			undo.redo();
+			macros_textarea.setCaretPosition(pos+1); 
+			macros_textarea.grabFocus();
+		    }
+		    if (!undo.canRedo()){
+			redo_button.setEnabled(false);
+		    }
+		    else{
+			redo_button.setEnabled(true); 
+		    }
+		}
+		catch (Exception exception) {
+		    DebugStream.printStackTrace(exception);
+		}
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ParsingProgress.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ParsingProgress.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/ParsingProgress.java	(revision 31635)
@@ -0,0 +1,101 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+/**************************************************************************************
+ * Title:        Gatherer
+ * Description:  The Gatherer: a tool for gathering and enriching a digital collection.
+ * Copyright:    Copyright (c) 2001
+ * Company:      The University of Waikato
+ * Written:      03/05/02
+ * Revised:      20/05/02
+ *               17/11/02 - Commented
+ **************************************************************************************/
+import java.awt.*;
+import javax.swing.*;
+
+/** This class provides a progress bar to be displayed whenever the module must reload the plugin and classifier information, either automatically or if the user indicates a reload is needed.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class ParsingProgress
+    extends JDialog {
+    /** The content pane within the dialog box. */
+    private JPanel content_pane = null;
+    /** The progress bar itself. */
+    private JProgressBar progress = null;
+    /** The default size of the progress dialog. */
+    static final Dimension SIZE = new Dimension(600,75);
+
+    /** Constructor.
+     * @param title The title to show on the dialog, as a <strong>String</strong>.
+     * @param message The message to show on the dialog, as a <strong>String</strong>.
+     * @param max The total number of plugins/classifiers that have to be parsed before the progress bar can be disposed of, as an <i>int</i>.
+     */
+    public ParsingProgress(String title, String message, int max) {
+	super();
+	this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+	this.setSize(SIZE);
+	this.setTitle(title);
+
+	// Creation
+	this.content_pane = (JPanel) getContentPane();
+	this.progress = new JProgressBar();
+	this.progress.setMaximum(max);
+
+	// Layout
+	this.content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	this.content_pane.setLayout(new BorderLayout());
+	this.content_pane.add(new JLabel(message), BorderLayout.NORTH);
+	this.content_pane.add(progress, BorderLayout.CENTER);
+
+	// Center and display
+	Dimension screen_size = Toolkit.getDefaultToolkit().getScreenSize();
+	setLocation((screen_size.width - SIZE.width) / 2,
+		    (screen_size.height - SIZE.height) / 2);
+	setVisible(true);
+    }
+
+    public void destroy() {
+    }
+
+    /** Method which increments the progress count by one, which should be called after every successful parsing of a classifier or plugin.
+     */
+    public void inc() {
+	progress.setValue(progress.getValue() + 1);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Plugin.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Plugin.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Plugin.java	(revision 31635)
@@ -0,0 +1,236 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.greenstone.Plugins;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.*;
+
+
+/** This class is responsible for storing information from a parsed pluginfo call in such a way that it allows easy access to parsed details for the purposes of user design and specification of plugins. */
+public class Plugin
+    extends ArgumentContainer
+{
+    private String default_block_expression = "";
+    private String default_process_expression = "";
+    private boolean does_explode_metadata_databases = false; 
+    private boolean does_replace_srcdocs_with_html = false; // to work with replace_srcdoc_with_html.pl
+    /** Plugins are loaded as needed. This variable indicates 1. whether a plugin has been loaded already; 2. whether a 
+     * plugin has been successfully loaded. When both are true, has_loaded_options will be true. */
+    private boolean has_loaded_options = false;
+
+    /** Constructor used in DOMProxyListModel initializations, and Library Level. Used for Base plugins (those in the list of available plugins, not ones that are in the DOMProxyList)
+     */
+    public Plugin() {
+    }
+
+    
+    public Plugin(Element element, Plugin base_plugin) {
+	super(element, base_plugin);
+    }
+
+
+    /** Method to compare two plugins for ordering.
+     * We override the base method cos we compare by plugin name rather than toString()
+     */
+    public int compareTo(Object object) {
+	if(object instanceof Plugin) {
+	    return name.compareTo(((Plugin)object).getName());
+	}
+	return -1;
+    }
+
+    
+    /** The assigned plugin constructor.
+     * @param element the DOM Element this plugin is based upon
+     */
+    public DOMProxyListEntry create(Element element) {
+	String plugin_name = element.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+	// Determine the base plugin from the plugin name
+	Plugin base_plugin = Plugins.getPlugin(plugin_name, true);
+	Plugin plugin = new Plugin(element, base_plugin);
+	if (base_plugin == null) {
+	    plugin.setAssigned(false);
+	}
+	base_plugin = null;
+	plugin_name = null;
+	return plugin;
+    }
+
+
+    public boolean hasLoadedOptions()
+    {
+	return has_loaded_options;
+    }
+
+
+    /** Checks whether the plugin this instance is based on processes metadata databases that can be exploded. */
+    public boolean doesExplodeMetadataDatabases()
+    {
+	Plugin base_plugin = Plugins.getPlugin(getName(), false);
+	if (base_plugin == null) {
+	    return false;
+	}
+	return base_plugin.does_explode_metadata_databases;
+    }
+
+    /** Checks whether the plugin this instance is based on processes source documents that can be replaced with their Greenstone generated html variants. 
+     * This method works with replace_srcdoc_with_html.pl */
+    public boolean doesReplaceSrcDocsWithHtml()
+    {
+	//return does_replace_srcdocs_with_html;
+	Plugin base_plugin = Plugins.getPlugin(getName(), false);
+	if (base_plugin == null) {
+	  return false;
+	}
+	return base_plugin.does_replace_srcdocs_with_html;
+    }
+
+    /** Checks whether this plugin instance will process the specified file (given its block_exp). */
+    public boolean doesBlockFile(File file)
+    {
+	// Check the filename against the plugin's block_exp value
+	ArrayList arguments = getArguments();
+	for (int i = 0; i < arguments.size(); i++) {
+	    Argument argument = (Argument) arguments.get(i);
+	    if (argument.getName().equals("block_exp")) {
+		// Try the assigned value first, for when the user has manually set the value
+		String regular_expression = argument.getValue();
+		if (regular_expression == null || regular_expression.equals("")) {
+		    // Not set, so use the default value
+		    regular_expression = argument.getDefaultValue();
+		    if (regular_expression.equals("")) {
+			continue;
+		    }
+		}
+
+		if (doesFileMatchRegularExpression(file, regular_expression)) {
+		    return true;
+		}
+	    }
+	}
+
+	// Try the plugin's default block expression
+	if (!default_block_expression.equals("") && doesFileMatchRegularExpression(file, default_block_expression)) {
+	    return true;
+	}
+
+	// This plugin will (probably) not deal with the specified file
+	return false;
+    }
+
+
+    /** Checks whether this plugin instance will process the specified file (given its process_exp). */
+    public boolean doesProcessFile(File file)
+    {
+	// Check the filename against the plugin's process_exp value
+	ArrayList arguments = getArguments();
+	for (int i = 0; i < arguments.size(); i++) {
+	    Argument argument = (Argument) arguments.get(i);
+	    if (argument.getName().equals("process_exp")) {
+		// Try the assigned value first, for when the user has manually set the value
+		String regular_expression = argument.getValue();
+		if (regular_expression == null || regular_expression.equals("")) {
+		    // Not set, so use the default value
+		    regular_expression = argument.getDefaultValue();
+		    if (regular_expression.equals("")) {
+			continue;
+		    }
+		}
+
+		if (doesFileMatchRegularExpression(file, regular_expression)) {
+		    return true;
+		}
+	    }
+	}
+
+	// Try the plugin's default process expression
+	if (!default_process_expression.equals("") && doesFileMatchRegularExpression(file, default_process_expression)) {
+	    return true;
+	}
+
+	// This plugin will (probably) not deal with the specified file
+	return false;
+    }
+
+
+    private boolean doesFileMatchRegularExpression(File file, String regular_expression)
+    {
+	// The $ at the end doesn't seem to work in Java, so need to add ".*" at the start
+	if (regular_expression.startsWith("(?i)")) {
+	    // Don't mess up case-insensitive matching though
+	    regular_expression = "(?i)" + ".*" + regular_expression.substring("(?i)".length());
+	}
+	else {
+	    regular_expression = ".*" + regular_expression;
+	}
+
+	// If the filename matches the regular expression, this plugin will deal with the file in some way
+	if (file.getName().matches(regular_expression)) {
+	    return true;
+	}
+
+	return false;
+    }
+
+
+    public boolean isSeparator() {
+	return (element != null && element.getAttribute(StaticStrings.SEPARATOR_ATTRIBUTE).equals(StaticStrings.TRUE_STR));
+    }
+
+
+    public void setDefaultBlockExpression(String default_block_expression)
+    {
+	this.default_block_expression = default_block_expression;
+    }
+
+
+    public void setDefaultProcessExpression(String default_process_expression)
+    {
+	this.default_process_expression = default_process_expression;
+    }
+
+
+    public void setDoesExplodeMetadataDatabases(boolean does_explode_metadata_databases)
+    {
+	this.does_explode_metadata_databases = does_explode_metadata_databases;
+    }
+
+    // To work with replace_srcdoc_with_html.pl
+    public void setDoesReplaceSrcDocsWithHtml(boolean does_replace_srcdocs_with_html)
+    {
+	this.does_replace_srcdocs_with_html = does_replace_srcdocs_with_html;
+    }
+
+    public void setHasLoadedOptions(boolean hasLoadedOptions)
+    {
+	this.has_loaded_options = hasLoadedOptions;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/PluginManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/PluginManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/PluginManager.java	(revision 31635)
@@ -0,0 +1,1060 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.plaf.*;
+import javax.swing.plaf.basic.*;
+import org.apache.xerces.parsers.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionContentsChangedListener;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.greenstone.Plugins;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.GComboBox;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.ModalDialog;
+import org.greenstone.gatherer.gui.WarningDialog;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+import org.xml.sax.*;
+
+
+/** This class is for maintaining a list of known plug-ins, and importing new plugins using the parser. */
+public class PluginManager
+    extends DOMProxyListModel
+    implements CollectionContentsChangedListener
+{
+    /** When asking how many rows are in the model, and if this variables value is true, then this modifier alters the number returned. This funtionality is used to hide the last three rows of the list in low detail modes. */
+    private boolean modify_row_count = false;
+    /** The controls for editing the contents of this manager. */
+    private Control controls = null;
+    private DOMProxyListModel model;
+    private JPanel separator;
+    private Plugin separator_plugin;
+
+  /** The number of plugins that are fixed 'below the line' */
+  private static int NUM_FIXED_PLUGINS = 3; 
+    /** Constructor.
+     */
+    public PluginManager() {	super(CollectionDesignManager.collect_config.getDocumentElement(), StaticStrings.PLUGIN_ELEMENT, new Plugin());
+	DebugStream.println("PluginManager: " + super.getSize() + " plugins parsed.");
+	model = this;
+
+	// Force the assigned plugins to be loaded and cached now
+	for (int i = 0; i < getSize(); i++) {
+	    getElementAt(i);
+	}
+	
+	// Create the separator, cause we can reuse it.
+	separator = getSeparator();
+	placeSeparator();
+	// check for MetadataXMLPlug
+	ensureMetadataXMLPlugIsLoaded();
+	// Listen for CollectionContentsChanged events, so we can give plugin hints when new files are added
+	CollectionManager.addCollectionContentsChangedListener(this);
+	
+    }
+
+    private void ensureMetadataXMLPlugIsLoaded() {
+
+	Plugin metaxmlplug = Plugins.getPlugin(StaticStrings.METADATAXMLPLUG_STR, false);
+	if (metaxmlplug!= null) {
+	    if (!contains(metaxmlplug)) {
+		// Create a new element in the DOM
+		Element new_plugin_element = CollectionConfiguration.createElement(StaticStrings.PLUGIN_ELEMENT);
+		new_plugin_element.setAttribute(StaticStrings.TYPE_ATTRIBUTE, metaxmlplug.getName());
+		Plugin new_plugin = new Plugin(new_plugin_element, metaxmlplug);
+		assignPlugin(new_plugin);
+	    }
+	}
+    }
+
+
+    /** Retrieve a list of the plugins that are available to be added to the collection. */
+    private Object[] getAvailablePlugins()
+    {
+	ArrayList available = new ArrayList();
+
+	// Add all the non-abstract core Greenstone plugins, except for ArcPlug and RecPlug
+	ArrayList plugins_list = Plugins.getPluginsList();
+	for (int i = 0; i < plugins_list.size(); i++) {
+	    Plugin plugin = (Plugin) plugins_list.get(i);
+	    if (!plugin.isAbstract()) {
+		String plugin_name = plugin.getName();
+		if (!plugin_name.equals(StaticStrings.ARCPLUG_STR) && !plugin_name.equals(StaticStrings.RECPLUG_STR) && !plugin_name.equals(StaticStrings.METADATAXMLPLUG_STR)) {
+		  available.add(plugin);
+		}
+	    }
+	}
+        
+	// Sort the available plugins into alphabetical order
+	Collections.sort(available);
+
+	return available.toArray();
+    }
+
+
+    public ArrayList getExploderPlugins(File file)
+    {
+	ArrayList exploder_plugins = new ArrayList();
+	ArrayList plugins_list = Plugins.getPluginsList();
+	for (int i = 0; i < plugins_list.size(); i++) {
+	    Plugin plugin = (Plugin) plugins_list.get(i);
+	    if (plugin.doesExplodeMetadataDatabases() == true && plugin.doesProcessFile(file)) {
+		exploder_plugins.add(plugin);
+	    }
+	}
+
+	return exploder_plugins;	
+    }
+    
+    public boolean isFileExplodable(File file)
+    {
+	ArrayList plugins_list = Plugins.getPluginsList();
+	for (int i = 0; i < plugins_list.size(); i++) {
+	    Plugin plugin = (Plugin) plugins_list.get(i);
+	    if (plugin.doesExplodeMetadataDatabases() == true && plugin.doesProcessFile(file) == true) { 
+		return true;
+	    }
+	}
+
+	return false;
+    }
+
+    // Works with replace_srcdoc_with_html.pl
+    public ArrayList getSrcReplacerPlugins(File file)
+    {
+	ArrayList srcreplacer_plugins = new ArrayList();
+	ArrayList plugins_list = Plugins.getPluginsList();
+	for (int i = 0; i < plugins_list.size(); i++) {
+	    Plugin plugin = (Plugin) plugins_list.get(i);
+	    if (plugin.doesReplaceSrcDocsWithHtml() == true && plugin.doesProcessFile(file)) {
+		srcreplacer_plugins.add(plugin);
+	    }
+	}
+
+	return srcreplacer_plugins;	
+    }	
+
+    // Works with replace_srcdoc_with_html.pl
+    public boolean isFileSrcReplaceable(File file)
+    {
+	ArrayList plugins_list = Plugins.getPluginsList();
+	for (int i = 0; i < plugins_list.size(); i++) {
+	    Plugin plugin = (Plugin) plugins_list.get(i);
+	   
+	    if (plugin.doesReplaceSrcDocsWithHtml() == true && plugin.doesProcessFile(file) == true) { 
+		return true;
+	    }
+	}
+
+	return false;
+    }	
+
+    /** Method to assign a plugin
+     * @param plugin the Plugin to assign
+     */
+    private void assignPlugin(Plugin plugin) {
+      if(plugin.getName().equals(StaticStrings.RECPLUG_STR) || plugin.getName().equals(StaticStrings.ARCPLUG_STR) || plugin.getName().equals(StaticStrings.METADATAXMLPLUG_STR)) {
+	    addAfter(plugin, separator_plugin); // Adds after separator
+	} else {
+	    addBefore(plugin, separator_plugin);
+	}
+    }
+
+
+    /** Destructor. */
+    public void destroy()
+    {
+	CollectionManager.removeCollectionContentsChangedListener(this);
+
+	if (controls != null) {
+	    controls.destroy();
+	    controls = null;
+	}
+    }
+
+
+    /** This function listens for new files being added to the collection and hints about suitable plugins. */
+    public void fileAddedToCollection(File file)
+    {
+	// First check the plugins already assigned in the collection
+	for (int i = 0; i < super.getSize(); i++) {
+	    Plugin assigned_plugin = (Plugin) getElementAt(i);
+	    if (assigned_plugin.isSeparator() == false && (assigned_plugin.doesProcessFile(file) == true || assigned_plugin.doesBlockFile(file) == true)) {
+		// This file will be processed by an assigned plugin, so no suggestion is necessary
+		DebugStream.println("Processed by assigned plugin: " + assigned_plugin);
+		return;
+	    }
+	}
+
+	// Next try the plugins NOT already assigned in the collection
+	ArrayList suitable_plugins = new ArrayList();
+	Object[] unassigned_plugins = getAvailablePlugins();
+	for (int i = 0; i < unassigned_plugins.length; i++) {
+	    Plugin unassigned_plugin = (Plugin) unassigned_plugins[i];
+	    if (unassigned_plugin.doesProcessFile(file) == true) {
+		DebugStream.println("Processed by unassigned plugin: " + unassigned_plugin);
+		suitable_plugins.add(unassigned_plugin);
+	    }
+	}
+
+	// If there appear to be no suitable plugins, warn the user about this and be done
+	if (suitable_plugins.size() == 0) {
+	    String[] args = new String[1];
+	    args[0] = file.getName();
+	    WarningDialog warning_dialog = new WarningDialog("warning.NoPluginExpectedToProcessFile", Dictionary.get("NoPluginExpectedToProcessFile.Title"), Dictionary.get("NoPluginExpectedToProcessFile.Message", args), null, false);
+	    warning_dialog.display();
+	    warning_dialog.dispose();
+	    return;
+	}
+
+	// Generate a dialog 
+	new PluginSuggestionPrompt(file.getName(), suitable_plugins);
+    }
+
+
+    /** Method to retrieve the control for this manager.
+     * @return the Control
+     */
+    public Control getControls() {
+	if(controls == null) {
+	    // Build controls
+	    controls = new PluginControl();
+	}
+	return controls;
+    }
+
+
+    /** Overrides getSize in DOMProxyListModel to take into account the row count modifier used to hide the last three rows in lower detail modes
+     * @return an int indicating the number of rows in the model, or more correctly the desired number of rows to display
+     */
+    public int getSize() {
+	int result = super.getSize();
+	if(modify_row_count) {
+	  result = result-(NUM_FIXED_PLUGINS+1);
+	}
+	return result;
+    }
+
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+	if(controls != null) {
+	    ((PluginControl)controls).modeChanged(mode);
+	}
+    }    
+
+    /** Method to move a plugin in the list order.
+     * @param plugin the Plugin you want to move.
+     * @param direction true to move the plugin up, false to move it down.
+     * @param all true to move to move all the way, false for a single step.
+     */
+    // why are all the error notices there when the buttons are disabled is you cant move???
+    private void movePlugin(Plugin plugin, boolean direction, boolean all) {
+	// Can't ever move RecPlug or ArcPlug
+      if(super.getSize() < (NUM_FIXED_PLUGINS+2)) {
+	    //DebugStream.println("Not enough plugins to allow moving.");
+	    return;
+	}
+	if(plugin.getName().equals(StaticStrings.ARCPLUG_STR) || plugin.getName().equals(StaticStrings.RECPLUG_STR) || plugin.getName().equals(StaticStrings.METADATAXMLPLUG_STR)) {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.Move.Fixed"), Dictionary.get("CDM.Move.Title"), JOptionPane.ERROR_MESSAGE);
+	    return;
+	}
+	if(all) {
+	    // Move to top
+	    if(direction) {
+		// Remove the moving plugin
+		remove(plugin);
+		// Retrieve the first plugin
+		Plugin first_plugin = (Plugin) getElementAt(0);
+		// Add the moving plugin before the first plugin
+		addBefore(plugin, first_plugin);
+		first_plugin = null;
+	    }
+	    else {
+		// Remove the moving plugin
+		remove(plugin);
+		// Add the moving plugin before the separator
+		addBefore(plugin, separator_plugin);
+	    }
+	}
+	else {
+	    // Try to move the plugin one step in the desired direction.
+	    int index = indexOf(plugin);
+	    ///ystem.err.println("Index of " + plugin + " = " + index);
+	    if(direction) {
+		index--;
+		if(index < 0) {
+		    String args[] = new String[2];
+		    args[0] = Dictionary.get("CDM.PlugInManager.PlugIn_Str");
+		    args[1] = plugin.getName();
+		    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.Move.At_Top", args), Dictionary.get("CDM.Move.Title"), JOptionPane.ERROR_MESSAGE);
+		    return;
+		}
+		remove(plugin);
+		add(index, plugin);
+	    }
+	    else {
+		index++;
+		Plugin next_plugin = (Plugin) getElementAt(index);
+		if(next_plugin.isSeparator()) {
+		    String args[] = new String[1];
+		    args[0] = plugin.getName();
+		    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.Move.Cannot", args), Dictionary.get("CDM.Move.Title"), JOptionPane.ERROR_MESSAGE);
+		    // Still not going to move RecPlug or ArcPlug.
+		    return;
+		}
+		remove(plugin);
+		add(index, plugin);
+	    }
+	}
+    }
+
+    /** We attempt to place the separator between the unfixed and the fixed plugins. Since we only know of two fixed plugins, we search for either of them, and place the separator before them.
+     */
+    private void placeSeparator() {
+	///ystem.err.println("Placing separator.");
+	int separator_index = super.getSize();
+	if(separator_index > 0) { 
+	    boolean found_fixed = false;
+	    int index = separator_index - 1;
+	    while(index > 0) {
+		Plugin plugin = (Plugin) getElementAt(index);
+		String name = plugin.getName();
+		if(name.equals(StaticStrings.RECPLUG_STR) || name.equals(StaticStrings.ARCPLUG_STR) || name.equals(StaticStrings.METADATAXMLPLUG_STR)) {
+		    found_fixed = true;
+		    index--;
+		}
+		else {
+		    if(found_fixed) {
+			separator_index = index + 1;
+			index = -1;
+		    }
+		    else {
+			index--;
+		    }
+		}
+		name = null;
+		plugin = null;
+	    }
+	}
+	Element element = CollectionConfiguration.createElement(StaticStrings.PLUGIN_ELEMENT);
+	element.setAttribute(StaticStrings.TYPE_ATTRIBUTE, StaticStrings.SEPARATOR_ATTRIBUTE);
+	element.setAttribute(StaticStrings.SEPARATOR_ATTRIBUTE, StaticStrings.TRUE_STR);
+	separator_plugin = new Plugin(element, null);
+	///atherer.println("Adding plugin separator at: " + separator_index);
+	add(separator_index, separator_plugin);
+    }
+
+    /** This method removes an assigned plugin. I was tempted to call it unassign, but remove is more consistant. Note that there is no way to remove a plugin from the library.
+     * @param plugin The <strong>Plugin</strong> to remove.
+     */
+    private void removePlugin(Plugin plugin) {
+	remove(plugin);
+  
+    }
+
+
+    /** Inform the model to hide/show the last four lines on the list.
+     * @param modify_row_count true to hide the last four lines, false otherwise
+     */
+    private void setHideLines(boolean modify_row_count) {
+	this.modify_row_count = modify_row_count;
+	int original_size = super.getSize();
+	if(modify_row_count) {
+	  fireIntervalRemoved(this, original_size - (NUM_FIXED_PLUGINS+2), original_size - 1);
+	}
+	else {
+	  fireIntervalAdded(this, original_size - (NUM_FIXED_PLUGINS+2), original_size - 1);
+	}
+    }
+
+    /** Determine the current separator index. */
+    private int findSeparatorIndex() {
+	int separator_index = super.getSize() - 1;
+	while(separator_index >= 0) {
+	    Plugin search = (Plugin) getElementAt(separator_index);
+	    if(search.isSeparator()) {
+		return separator_index;
+	    }
+	    separator_index--;
+	}
+	return separator_index;
+    }
+
+
+    /** A class which provodes controls for assigned and editing plugins. */
+    private class PluginControl
+	extends JPanel
+	implements Control {
+	/** Button for adding plugins. */
+	private JButton add = null;
+	/** Button for configuring the selected plugin. */
+	private JButton configure = null;
+	/** Button to move an assigned plugin one position lower in the order. */
+	private JButton move_down_button = null;
+	/** Button to move an assigned plugin one position higher in the order. */
+	private JButton move_up_button = null;
+	/** Button to remove the selected plugin. */
+	private JButton remove = null;
+	/** A combobox containing all of the known plugins, including those that may have already been assigned. */
+	private JComboBox plugin_combobox = null;
+	/** The label next to the plugin combobox. */
+	private JLabel plugin_label = null;
+	/** The label above the assigned plugin list. */
+	private JLabel plugin_list_label = null;
+	/** A list of assigned plugins. */
+	private JList plugin_list = null;
+	/** The area where the add, configure and remove buttons are placed. */
+	private JPanel button_pane = null;
+	/** The region which divides the central portion of the view into list and controls */
+	private JPanel central_pane = null;
+	/** The area where movement buttons are placed. */
+	private JPanel movement_pane = null;
+	/** The small region containing the plugin combobox and its label. */
+	private JPanel plugin_pane = null;
+	/** The pane containing the assigned plugin list and its label. */
+	private JPanel plugin_list_pane = null;
+
+	/** Constructor.
+	 */
+	public PluginControl()
+	{
+	    // Create
+	    add = new GLIButton(Dictionary.get("CDM.PlugInManager.Add"), Dictionary.get("CDM.PlugInManager.Add_Tooltip"));
+	    
+	    button_pane = new JPanel();
+	    central_pane = new JPanel();
+            button_pane.setComponentOrientation(Dictionary.getOrientation());
+            central_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    configure = new GLIButton(Dictionary.get("CDM.PlugInManager.Configure"), Dictionary.get("CDM.PlugInManager.Configure_Tooltip"));
+	    configure.setEnabled(false);
+	    
+	    JPanel header_pane = new DesignPaneHeader("CDM.GUI.Plugins", "plugins");
+	    move_up_button = new GLIButton(Dictionary.get("CDM.Move.Move_Up"), JarTools.getImage("arrow-up.gif"), Dictionary.get("CDM.Move.Move_Up_Tooltip"));
+	    move_up_button.setEnabled(false);
+	    
+	    move_down_button = new GLIButton(Dictionary.get("CDM.Move.Move_Down"), JarTools.getImage("arrow-down.gif"), Dictionary.get("CDM.Move.Move_Down_Tooltip"));
+	    move_down_button.setEnabled(false);
+	    
+	    movement_pane = new JPanel();
+            movement_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    PluginComboboxListener picl = new PluginComboboxListener();
+	    plugin_combobox = new JComboBox(getAvailablePlugins());
+	    plugin_combobox.setOpaque(!Utility.isMac());
+	    plugin_combobox.setEditable(false);
+            plugin_combobox.setComponentOrientation(Dictionary.getOrientation());
+            
+	    picl.itemStateChanged(new ItemEvent(plugin_combobox, 0, null, ItemEvent.SELECTED));
+
+	    plugin_label = new JLabel(Dictionary.get("CDM.PlugInManager.PlugIn"));
+	    plugin_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    plugin_list = new JList(model);
+	    plugin_list.setOpaque(true);
+	    plugin_list.setCellRenderer(new ListRenderer());
+	    plugin_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+	    plugin_list.setComponentOrientation(Dictionary.getOrientation());
+            
+            plugin_list_label = new JLabel(Dictionary.get("CDM.PlugInManager.Assigned"));
+	    //plugin_list_label.setHorizontalAlignment(JLabel.CENTER);
+	    plugin_list_label.setOpaque(true);
+	    plugin_list_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    plugin_list_pane = new JPanel();
+            plugin_list_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    plugin_pane = new JPanel();
+	    plugin_pane.setOpaque(true);
+            plugin_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    remove = new GLIButton(Dictionary.get("CDM.PlugInManager.Remove"), Dictionary.get("CDM.PlugInManager.Remove_Tooltip"));
+	    remove.setEnabled(false);
+	    
+	    // Listeners
+	    add.addActionListener(new AddListener());
+	    configure.addActionListener(new ConfigureListener());
+	    MoveListener ml = new MoveListener();
+	    move_down_button.addActionListener(ml);
+	    move_down_button.addActionListener(CollectionDesignManager.all_change_listener);
+	    move_up_button.addActionListener(ml);
+	    move_up_button.addActionListener(CollectionDesignManager.all_change_listener);
+	    plugin_combobox.addItemListener(picl);
+	    remove.addActionListener(new RemoveListener());
+	    remove.addActionListener(CollectionDesignManager.all_change_listener);
+	    plugin_list.addMouseListener(new ClickListener());
+	    plugin_list.addListSelectionListener(new ListListener());
+	    picl = null;
+
+	    // Layout
+	    movement_pane.setBorder(BorderFactory.createEmptyBorder(0,2,0,0));
+	    movement_pane.setLayout(new GridLayout(4,1));
+	    movement_pane.add(move_up_button);
+            JPanel tmp = new JPanel();
+            tmp.setComponentOrientation(Dictionary.getOrientation());
+	    movement_pane.add(tmp);
+            tmp = new JPanel();
+            tmp.setComponentOrientation(Dictionary.getOrientation());
+	    movement_pane.add(tmp);
+	    movement_pane.add(move_down_button);
+
+	    plugin_list_pane.setLayout(new BorderLayout());
+	    plugin_list_pane.add(plugin_list_label, BorderLayout.NORTH);
+	    plugin_list_pane.add(new JScrollPane(plugin_list), BorderLayout.CENTER);
+	    plugin_list_pane.add(movement_pane, BorderLayout.LINE_END);
+	    modeChanged(Configuration.getMode()); // Whether the special plugins are hidden or not is mode dependant
+
+	    plugin_label.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+
+	    plugin_pane.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));
+	    plugin_pane.setLayout(new BorderLayout(5,0));
+	    plugin_pane.add(plugin_label, BorderLayout.LINE_START);
+	    plugin_pane.add(plugin_combobox, BorderLayout.CENTER);
+
+	    button_pane.setLayout(new GridLayout(1,3));
+	    button_pane.add(add);
+	    button_pane.add(configure);
+	    button_pane.add(remove);
+
+	    // Scope these mad bordering skillz.
+	    JPanel temp = new JPanel(new BorderLayout());
+	    temp.add(plugin_pane, BorderLayout.NORTH);
+	    temp.add(button_pane, BorderLayout.SOUTH);
+            temp.setComponentOrientation(Dictionary.getOrientation());
+            
+	    central_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    central_pane.setLayout(new BorderLayout());
+	    central_pane.add(plugin_list_pane, BorderLayout.CENTER);
+	    central_pane.add(temp, BorderLayout.SOUTH);
+
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(header_pane, BorderLayout.NORTH);
+	    add(central_pane, BorderLayout.CENTER);
+	}
+
+	/** Method which acts like a destructor, tidying up references to persistant objects.
+	 */
+	public void destroy() {
+	}
+
+	/** This method is overridden to ensure the instructions are scrolled to top, before the super classes updateUI() is called.
+	    no longer have instructions, do we still need updateUI???
+	 */
+	public void gainFocus() {
+	    super.updateUI();
+	}
+
+	public void loseFocus() {
+	}
+
+	/** The current detail mode controls two aspects of plugin manager: whether the movement buttons are visible and whether the fixed position plugins Arc and RecPlug are in the list
+	 * @param mode the current mode as an int, which can be matched against static ints in the Configuration class
+	 */
+	public void modeChanged(int mode) {
+	    // First of all we clear the current selection, as there can be some serious problems if the user selects the plugins we're hiding, or had the last plugin selected before we unhid the last three
+	    plugin_list.clearSelection();	 
+	    plugin_combobox.setModel(new DefaultComboBoxModel(getAvailablePlugins()));
+	    setHideLines(!(mode >= Configuration.EXPERT_MODE));
+
+	    plugin_list_pane.updateUI();
+	}
+
+
+	private class AddListener
+	    implements ActionListener
+	{
+	    public void actionPerformed(ActionEvent event)
+	    {
+		if (plugin_combobox.getSelectedItem() != null) {
+		    // This must be done on a new thread for the remote building code
+		    new AddPluginTask(plugin_combobox.getSelectedItem().toString()).start();
+		}
+	    }
+	}
+
+
+	private class AddPluginTask
+	    extends Thread
+	{
+	    private String plugin_name;
+
+	    public AddPluginTask(String plugin_name)
+	    {
+		this.plugin_name = plugin_name;
+	    }
+
+	    public void run()
+	    {
+		// Retrieve the plugin
+		Plugin plugin = Plugins.getPlugin(plugin_name, true);
+		if (plugin == null) {
+		    System.err.println("Error: getPlugin() returned null.");
+		    return;
+		}
+
+		// Create a new element in the DOM
+		Element new_plugin_element = CollectionConfiguration.createElement(StaticStrings.PLUGIN_ELEMENT);
+		new_plugin_element.setAttribute(StaticStrings.TYPE_ATTRIBUTE, plugin.getName());
+		Plugin new_plugin = new Plugin(new_plugin_element, plugin);
+
+		ArgumentConfiguration ac = new ArgumentConfiguration(new_plugin);
+		ac.addOKButtonActionListener(CollectionDesignManager.all_change_listener);
+		if (ac.display()) {
+		    assignPlugin(new_plugin);
+		    plugin_list.setSelectedValue(new_plugin, true);
+		}
+	    }
+	}
+
+
+	/** Listens for double clicks apon the list and react as if the configure button was pushed. */
+	private class ClickListener
+	    extends MouseAdapter {
+	    /** Called whenever the mouse is clicked over a registered component, we use this to chain through to the configure prompt.
+	     * @param event A <strong>MouseEvent</strong> containing information about the mouse click.
+	    */
+	    public void mouseClicked(MouseEvent event) {
+		if(event.getClickCount() == 2 ) {
+		    if(!plugin_list.isSelectionEmpty()) {
+			Plugin plugin = (Plugin) plugin_list.getSelectedValue();
+			if(!plugin.isSeparator()) {
+			    ArgumentConfiguration ac = new ArgumentConfiguration(plugin);
+			    ac.addOKButtonActionListener(CollectionDesignManager.all_change_listener);
+			    if (ac.display()) {
+				refresh(plugin);
+			    }
+			    ac.destroy();
+			    ac = null;
+			}
+		    }
+		}
+	    }
+	}
+
+	/** This class listens for actions upon the configure button in the controls, and if detected creates a new <strong>ArgumentConfiguration</strong> dialog box to allow for configuration.
+	 * @see org.greenstone.gatherer.cdm.ArgumentConfiguration
+	 */
+	private class ConfigureListener
+	    implements ActionListener {
+	    /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured on one of our target controls.
+	     * @param event An <strong>ActionEvent</strong> containing information garnered from the control action.
+	     */
+	    public void actionPerformed(ActionEvent event) {
+		if(!plugin_list.isSelectionEmpty()) {
+		    Plugin plugin = (Plugin) plugin_list.getSelectedValue();
+		    if(!plugin.isSeparator()) {
+			ArgumentConfiguration ac = new ArgumentConfiguration(plugin);
+			ac.addOKButtonActionListener(CollectionDesignManager.all_change_listener);
+			if (ac.display()) {
+			    refresh(plugin);
+			}
+			ac.destroy();
+			ac = null;
+		    }
+		}
+	    }
+	}
+
+  /** listens for changes in the list selection and enables the configure and remove buttons if there is a selection, disables them if there is no selection */
+  private class ListListener
+    implements ListSelectionListener {
+    
+    public void valueChanged(ListSelectionEvent e) {
+      if (!e.getValueIsAdjusting()) { // we get two events for one change in list selection - use the false one (the second one)
+        if (plugin_list.isSelectionEmpty()) {
+          move_up_button.setEnabled(false);
+          move_down_button.setEnabled(false);
+          configure.setEnabled(false);
+          remove.setEnabled(false);
+        } else {
+          Plugin selected_plugin = (Plugin) plugin_list.getSelectedValue();
+          if(selected_plugin.isSeparator()) {
+            move_up_button.setEnabled(false);
+            move_down_button.setEnabled(false);
+            configure.setEnabled(false);
+            remove.setEnabled(false);
+          } else {
+            configure.setEnabled(true);
+            String plugin_name = selected_plugin.getName();
+            // Some buttons are only available for plugins other than ArcPlug and RecPlug
+            if(plugin_name.equals(StaticStrings.ARCPLUG_STR) || plugin_name.equals(StaticStrings.RECPLUG_STR) || plugin_name.equals(StaticStrings.METADATAXMLPLUG_STR)) {
+              move_up_button.setEnabled(false);
+              move_down_button.setEnabled(false);
+              remove.setEnabled(false);
+            } 
+
+            else {
+              // don't let people remove special plugins such GreenstoneXMLPlug and GreenstoneMETSPlug,
+              // unless they are in expert mode or above
+              int mode = Configuration.getMode();
+              for (int i=0; i<StaticStrings.KEEP_PLUG.length; i++) {
+                if ((plugin_name.equals(StaticStrings.KEEP_PLUG[i])) &&
+                  (mode < Configuration.EXPERT_MODE)) {
+                  remove.setEnabled(false);
+                  break;
+                } else {
+                  remove.setEnabled(true);
+                }
+              }
+              
+              // Move ups are only enabled if the selected plugin isn't already at the top
+              Plugin first_plugin = (Plugin) getElementAt(0);
+              if(!first_plugin.equals(selected_plugin)) {
+                move_up_button.setEnabled(true);
+              } else {
+                move_up_button.setEnabled(false);
+              }
+              // And move downs are only allowed when the selected plugin isn't at an index one less than the separator line.
+              int separator_index = findSeparatorIndex();
+              int selected_index = plugin_list.getSelectedIndex();
+              if(selected_index < separator_index - 1) {
+                move_down_button.setEnabled(true);
+              } else {
+                move_down_button.setEnabled(false);
+              }
+            }
+            selected_plugin = null;
+            plugin_name = null;
+          }
+        }
+      }
+    }
+  }
+
+	/** A special list renderer which is able to render separating lines as well. */
+	private class ListRenderer
+	    extends DefaultListCellRenderer {
+	    /** Return a component that has been configured to display the specified value. That component's paint method is then called to "render" the cell. If it is necessary to compute the dimensions of a list because the list cells do not have a fixed size, this method is called to generate a component on which getPreferredSize  can be invoked.
+	     * @param list - The <strong>JList</strong> we're painting.
+	     * @param value - The value returned by list.getModel().getElementAt(index) as an <strong>Object</strong>.
+	     * @param index - The cells index as an <i>int</i>.
+	     * @param isSelected - <i>true</i> if the specified cell was selected.
+	     * @param cellHasFocus - <i>true</i> if the specified cell has the focus.
+	     * @return A <strong>Component</strong> whose paint() method will render the specified value.
+	     * @see javax.swing.JList
+	     * @see javax.swing.JSeparator
+	     * @see javax.swing.ListModel
+	     * @see javax.swing.ListSelectionModel
+	     */
+	    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+		Plugin plugin = (Plugin) value;
+		if(plugin.isSeparator()) {
+		    return separator;
+		}
+		else {
+		    return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+		}
+	    }
+	}
+
+
+	/** Listens for actions apon the move buttons in the manager controls, and if detected calls the <i>movePlugin()</i> method of the manager with the appropriate details. */
+	private class MoveListener
+	    implements ActionListener {
+	    /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured on one of our target controls.
+	     * @param event An <strong>ActionEvent</strong> containing information garnered from the control action.
+	     */
+	    public void actionPerformed(ActionEvent event) {
+		if (!plugin_list.isSelectionEmpty()) {
+		    Object object = plugin_list.getSelectedValue();
+		    if (object instanceof Plugin) {
+			Plugin plugin = (Plugin) object;
+			if (event.getSource() == move_up_button) {
+			    movePlugin(plugin, true, false);
+			}
+			else if (event.getSource() == move_down_button) {
+			    movePlugin(plugin, false, false);
+			}
+			plugin_list.setSelectedValue(plugin, true);
+		    }
+		}
+	    }
+	}
+
+	/** This listener reacts to changes in the current selection of the plugin combobox. */
+	private class PluginComboboxListener
+        implements ItemListener {
+        /** When a user selects a certain plugin, update the tooltip to show the plugin description. */
+        public void itemStateChanged(ItemEvent event) {
+            if(event.getStateChange() == ItemEvent.SELECTED) {
+                // Retrieve the selected plugin
+                Object current_selection = plugin_combobox.getSelectedItem();
+                // And reset the tooltip. If the plugin is null or is a string, then go back to the default message
+                if(current_selection == null || current_selection instanceof String) {
+		    plugin_combobox.setToolTipText(Dictionary.get("CDM.PlugInManager.PlugIn_Tooltip"));
+                }
+                else {
+                    Plugin current_plugin = (Plugin) current_selection;
+		    plugin_combobox.setToolTipText(Utility.formatHTMLWidth(current_plugin.getDescription(), 40));
+                    current_plugin = null;
+                }
+                current_selection = null;
+            }
+        }
+    }
+	
+  /** This class listens for actions upon the remove button in the controls, and if detected calls the <i>removePlugin()</i> method.
+   */
+  private class RemoveListener
+    implements ActionListener {
+    /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured on one of our target controls.
+     * @param event An <strong>ActionEvent</strong> containing information garnered from the control action.
+     */
+    public void actionPerformed(ActionEvent event) {
+      int selected_index = plugin_list.getSelectedIndex();
+      if(selected_index != -1) {
+        Plugin selected_plugin = (Plugin) plugin_list.getSelectedValue();
+        removePlugin(selected_plugin);
+        selected_plugin = null;
+        // Refresh the available plugins
+        plugin_combobox.setModel(new DefaultComboBoxModel(getAvailablePlugins()));
+
+        // Select the next plugin if available
+        if(selected_index < plugin_list.getModel().getSize()) {
+          // If the new selection is above the separator we can remove it
+          if(selected_index < findSeparatorIndex()) {
+            plugin_list.setSelectedIndex(selected_index);
+            
+            // don't let people remove special plugins such GAPlug an METSPlug,
+            // unless they are in expert mode or above
+            int mode = Configuration.getMode();
+            for (int i=0; i<StaticStrings.KEEP_PLUG.length; i++) {
+              String selected_plugin_name
+                = ((Plugin)plugin_list.getSelectedValue()).getName();
+              
+              if (selected_plugin_name.equals(StaticStrings.METADATAXMLPLUG_STR)) {
+                //this plug cannot be removed under any conditions.
+                remove.setEnabled(false);
+              }else if ((selected_plugin_name.equals(StaticStrings.KEEP_PLUG[i])) &&
+                (mode < Configuration.EXPERT_MODE)) {
+                remove.setEnabled(false);
+                break;
+              } else {
+                remove.setEnabled(true);
+              }
+            }
+          }
+          // Otherwise select the first non-removable plugin
+          else {
+            plugin_list.setSelectedIndex(selected_index + 1);
+            remove.setEnabled(false);
+          }
+        } else {
+          remove.setEnabled(false);
+        }
+      } else {
+        remove.setEnabled(false);
+      }
+    }
+  }
+    }
+    
+
+    private class PluginSuggestionPrompt
+	extends ModalDialog
+	implements ActionListener
+    {
+	private Dimension size = new Dimension(480, 240);
+	private GComboBox suitable_plugins_combobox = null;
+	private GLIButton add_button = null;
+	private GLIButton ignore_button = null;
+
+	public PluginSuggestionPrompt(String filename, ArrayList suitable_plugins)
+	{
+	    super(Gatherer.g_man, true);
+	    setModal(true);
+	    setSize(size);
+	    setTitle(Dictionary.get("CDM.PluginManager.SuggestedPluginListTitle"));
+            this.setComponentOrientation(Dictionary.getOrientation());
+            
+	    String[] args = new String[1];
+	    args[0] = filename;
+
+	    JTextArea instructions_textarea = new JTextArea(Dictionary.get("CDM.PluginManager.Plugin_Suggestion_Prompt", args));
+	    instructions_textarea.setCaretPosition(0);
+	    instructions_textarea.setEditable(false);
+	    instructions_textarea.setLineWrap(true);
+	    instructions_textarea.setRows(5);
+	    instructions_textarea.setWrapStyleWord(true);
+            instructions_textarea.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JLabel suitable_plugins_label = new JLabel(Dictionary.get("CDM.PlugInManager.PlugIn"));
+	    suitable_plugins_label.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+            suitable_plugins_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    suitable_plugins_combobox = new GComboBox(suitable_plugins);
+	    suitable_plugins_combobox.setBackgroundNonSelectionColor(Configuration.getColor("coloring.editable_background", false));
+	    suitable_plugins_combobox.setBackgroundSelectionColor(Configuration.getColor("coloring.collection_selection_background", false));
+	    suitable_plugins_combobox.setTextNonSelectionColor(Configuration.getColor("coloring.workspace_tree_foreground", false));
+	    suitable_plugins_combobox.setTextSelectionColor(Configuration.getColor("coloring.collection_selection_foreground", false));
+            suitable_plugins_combobox.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel suitable_plugins_pane = new JPanel();
+            suitable_plugins_pane.setComponentOrientation(Dictionary.getOrientation());
+	    //suitable_plugins_pane.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));
+	    suitable_plugins_pane.setLayout(new BorderLayout(5,0));
+	    suitable_plugins_pane.add(suitable_plugins_label, BorderLayout.LINE_START);
+	    suitable_plugins_pane.add(suitable_plugins_combobox, BorderLayout.CENTER);
+
+	    add_button = new GLIButton(Dictionary.get("CDM.PlugInManager.QuickAdd"), Dictionary.get("CDM.PlugInManager.Add_Tooltip"));
+	    ignore_button = new GLIButton(Dictionary.get("CDM.PlugInManager.Ignore"), Dictionary.get("CDM.PlugInManager.Ignore_Tooltip"));
+	    
+	    add_button.addActionListener(this);
+	    ignore_button.addActionListener(this);
+
+	    JPanel button_pane = new JPanel();
+	    button_pane.setLayout(new GridLayout(1,2,5,0));
+	    button_pane.add(add_button);
+	    button_pane.add(ignore_button);
+            button_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel controls_pane = new JPanel();
+	    controls_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    controls_pane.setLayout(new GridLayout(2,1,0,5));
+	    controls_pane.add(suitable_plugins_pane);
+	    controls_pane.add(button_pane);
+            controls_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel content_pane = (JPanel) getContentPane();
+            content_pane.setComponentOrientation(Dictionary.getOrientation());
+	    content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    content_pane.setLayout(new BorderLayout());
+	    content_pane.add(instructions_textarea, BorderLayout.CENTER);
+	    content_pane.add(controls_pane, BorderLayout.SOUTH);
+
+	    // Show
+	    Dimension screen_size = Configuration.screen_size;
+	    setLocation((screen_size.width - size.width) / 2, (screen_size.height - size.height) / 2);
+	    setVisible(true);
+	}
+
+
+	public void actionPerformed(ActionEvent event)
+	{
+	    // Close the dialog
+	    setVisible(false);
+
+	    if (event.getSource() == add_button) {
+		AddPluginWithoutConfigurationTask add_plugin_task = new AddPluginWithoutConfigurationTask(suitable_plugins_combobox.getSelectedItem().toString());
+		add_plugin_task.start();
+
+		// We have to wait for this task to complete before continuing, otherwise
+		// we might offer to add the same plugin again before the task has completed
+		try {
+		    add_plugin_task.join();
+		}
+		catch (Exception exception) {
+		    DebugStream.printStackTrace(exception);
+		}
+	    }
+	}
+
+
+	private class AddPluginWithoutConfigurationTask
+	    extends Thread
+	{
+	    private String plugin_name;
+
+	    public AddPluginWithoutConfigurationTask(String plugin_name)
+	    {
+		this.plugin_name = plugin_name;
+	    }
+
+	    public void run()
+	    {
+		// Retrieve the plugin
+		Plugin plugin = Plugins.getPlugin(plugin_name, true);
+		if (plugin == null) {
+		    System.err.println("Error: getPlugin() returned null.");
+		    return;
+		}
+
+		// Create a new element in the DOM
+		Element new_plugin_element = CollectionConfiguration.createElement(StaticStrings.PLUGIN_ELEMENT);
+		new_plugin_element.setAttribute(StaticStrings.TYPE_ATTRIBUTE, plugin.getName());
+		Plugin new_plugin = new Plugin(new_plugin_element, plugin);
+
+		// Just assign the plugin with no argument configuration
+		assignPlugin(new_plugin);
+		Gatherer.c_man.getCollection().cdm.save();
+	    }
+	}
+    }
+
+
+    /** Creates a list separator.
+     * Found on Google Groups. Code courtesy of Paul Farwell.
+     */
+    private JPanel getSeparator() {
+	// We put the separator inside a panel to control its appearance
+	JPanel _sepPanel = new JPanel();
+	_sepPanel.setOpaque(false);
+	_sepPanel.setBorder(BorderFactory.createEmptyBorder(1, 3, 1, 3));
+	_sepPanel.setLayout(new BoxLayout(_sepPanel, BoxLayout.Y_AXIS));
+	_sepPanel.add(Box.createRigidArea(new Dimension(0, 4)));
+	// We have to be a little careful here, as the default UI for separators under MacOS is a blank box. Instead we force a BasicUI look
+	_sepPanel.add(new BasicSeparator());
+	_sepPanel.add(Box.createRigidArea(new Dimension(0, 4)));
+	return _sepPanel;
+    }
+
+    /** This class behaves just like a normal JSeparator except that, no matter what the current settings in the UIManager are, it always paints itself with BasicSeparatorUI. */
+    private static class BasicSeparator
+	extends JSeparator {
+
+	private ComponentUI basic_ui;
+
+	public BasicSeparator() {
+	    super();
+	    basic_ui = new BasicSeparatorUI();
+	}
+
+	public void paintComponent(Graphics g) {
+	    if (basic_ui != null) {
+		basic_ui.update(g, this);
+	    }   
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SearchMetadataManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SearchMetadataManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SearchMetadataManager.java	(revision 31635)
@@ -0,0 +1,368 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.table.*;
+import java.util.ArrayList;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.util.StaticStrings;
+
+
+public class SearchMetadataManager
+{
+    private Control controls;
+
+    public static final String TYPE_INDEX = "index";
+    public static final String TYPE_LEVEL = "level";
+    public static final String TYPE_PARTITION = "partition";
+    public static final String TYPE_LANGUAGE = "language";
+
+  // the following are special and should not be translated
+  public static final String METADATA_INDEX = "ex.metadata";
+
+    CollectionMetaManager collmeta_manager = CollectionDesignManager.collectionmeta_manager;
+
+    public SearchMetadataManager() {
+    }
+
+    /** Destructor. */
+    public void destroy() {
+	if (controls != null) {
+	    controls.destroy();
+	    controls = null;
+	}
+    }
+
+    public void loseFocus() {
+    }
+
+    public void gainFocus() {
+    }
+    
+    public Control getControls() {
+	if (controls == null) {
+	    controls = new DisplayControl();
+	}
+	return controls;
+    }
+    
+
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+    }
+
+    private class DisplayControl
+	extends JPanel
+	implements Control {
+
+	private SearchMetadataTable metadata_table = null;
+	
+	public DisplayControl() {
+	    super();
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    JPanel header_panel = new DesignPaneHeader("CDM.GUI.SearchMetadata", "searchmetadatasettings");
+
+	    metadata_table = new SearchMetadataTable();
+
+	    JScrollPane scroll_panel = new JScrollPane(metadata_table);
+	    scroll_panel.getViewport().setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    scroll_panel.setOpaque(true);
+
+	    JPanel metadata_table_pane = new JPanel();
+            metadata_table_pane.setComponentOrientation(Dictionary.getOrientation());
+	    metadata_table_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    metadata_table_pane.setLayout(new BorderLayout());
+	    metadata_table_pane.add(scroll_panel, BorderLayout.CENTER);
+
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(header_panel, BorderLayout.NORTH);
+	    add(metadata_table_pane, BorderLayout.CENTER);
+	}
+
+	public void loseFocus(){
+	    // save the last metadata in case its still editing
+	    metadata_table.stopEditing();
+	}
+	
+	
+	public void gainFocus(){
+	    // refresh the model as we may have changed indexes in the meantime
+	    metadata_table.refreshModel();
+	}
+
+	public void destroy() {}
+    }
+    
+    private ArrayList getEntries() {
+	ArrayList entries = new ArrayList();
+	
+	ArrayList indexes = CollectionDesignManager.index_manager.getIndexes();
+	if (indexes != null) {
+	    int indexes_size = indexes.size();
+	    for (int i=0; i<indexes_size; i++) {
+	      String index_id = ((Index)indexes.get(i)).getID();
+	      if (!index_id.equals(METADATA_INDEX)) {
+		SearchMetadataEntry sme = new SearchMetadataEntry(index_id, TYPE_INDEX);
+		entries.add(sme);
+	      }
+	    }
+	}
+	ArrayList levels = CollectionDesignManager.index_manager.getLevels();
+	if (levels != null) {
+	    int levels_size = levels.size();
+	    for (int i=0; i<levels_size; i++) {
+		SearchMetadataEntry sme = new SearchMetadataEntry(((IndexOption)levels.get(i)).getName(), TYPE_LEVEL);
+		entries.add(sme);
+	    }
+	}
+	
+	ArrayList partitions = CollectionDesignManager.subcollectionindex_manager.getSubcollectionIndexes();
+	if (partitions != null) {
+	    int partitions_size = partitions.size();
+	    for(int i=0; i<partitions_size; i++) {
+		SearchMetadataEntry sme = new SearchMetadataEntry(((SubcollectionIndex)partitions.get(i)).getID(), TYPE_PARTITION);
+		entries.add(sme);
+	    }
+	}
+	
+	ArrayList languages = CollectionDesignManager.language_manager.getLanguages();
+	if (languages != null) {
+	    int languages_size = languages.size();
+	    for (int i=0; i<languages_size; i++) {
+		SearchMetadataEntry sme = new SearchMetadataEntry(((Language)languages.get(i)).getCode(), TYPE_LANGUAGE);
+		entries.add(sme);
+	    }
+	}
+	return entries;
+    }
+    
+    
+    private class SearchMetadataEntry {
+		
+	String id;
+	String type;
+	CollectionMeta coll_meta = null;
+	String value;
+
+	public SearchMetadataEntry(String id, String type) {
+	    this.id = id;
+	    this.type = type;
+	    coll_meta = collmeta_manager.getMetadatum(getMetaID());
+	    value = coll_meta.getValue(CollectionMeta.GREENSTONE);
+	}
+	
+	public String toString() {
+	    return Dictionary.get("CDM.SearchMetadataManager.Type_"+type)+": "+id;
+	}
+	
+	public String getMetaID() {
+	  if (Gatherer.GS3) {
+	    // we don't use any dots in gs3
+	    return id;
+	  }
+	  return StaticStrings.STOP_CHARACTER+id;
+	  
+	}
+	public String getValue() {
+	    return value;
+	}
+	public void setValue(String val) {
+	    coll_meta.setValue(val);
+	    value = val;
+	}
+    }
+
+    private class SearchMetadataTable
+	extends JTable {
+
+	private SearchMetadataTableModel model = null;
+	
+	public SearchMetadataTable() {
+	    // create the model
+	  this.setComponentOrientation(Dictionary.getOrientation());
+	  
+	    model = new SearchMetadataTableModel();
+	    setModel(model);
+
+	    setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+	    // set our own cell renderer
+	    TableColumnModel column_model = getColumnModel();
+	    TableColumn name_column = column_model.getColumn(0);
+	    TableColumn value_column = column_model.getColumn(1);
+
+	    SearchMetadataTableCellRenderer cell_renderer = new SearchMetadataTableCellRenderer();
+	    name_column.setCellRenderer(cell_renderer);
+	    value_column.setCellRenderer(cell_renderer);
+	}
+	
+	public void refreshModel() {
+	    model.refresh();
+	}
+
+	public void stopEditing() {
+	    // Save the current value in the text field, then remove the editor so it doesn't get saved again
+	    TableCellEditor table_cell_editor = getCellEditor();
+	    if (table_cell_editor != null) {
+		table_cell_editor.stopCellEditing();
+	    }
+	}
+    }
+    
+    private class SearchMetadataTableModel
+	extends AbstractTableModel {
+	
+	// the list of items in the table
+	private ArrayList search_metadata_entries = null;
+	
+	final private String[] COLUMN_NAMES = {Dictionary.get("CDM.SearchMetadataManager.Component"), Dictionary.get("CDM.SearchMetadataManager.Component_Name")};
+	
+	public SearchMetadataTableModel() {
+	    refresh(); 
+	}
+	
+	/** Returns the number of columns in this table. */
+	public int getColumnCount() {
+	    
+	    return COLUMN_NAMES.length;
+	}
+	
+	/** Retrieves the name of the specified column. */
+	public String getColumnName(int col) {
+	    
+	    return COLUMN_NAMES[col];
+	}
+	
+	/** Returns the number of rows in this table. */
+	public int getRowCount() {
+	    
+	    return search_metadata_entries.size();
+	}
+	
+	/** Returns the cell value at a given row and column as an Object. */
+	public Object getValueAt(int row, int col) {
+	    // Check values are reasonable
+	    if (row < 0 || row >= search_metadata_entries.size() || col < 0 || col >= COLUMN_NAMES.length) {
+		return null;
+	    }
+	    
+	    SearchMetadataEntry sme = (SearchMetadataEntry) search_metadata_entries.get(row);
+	    if (col == 0) {
+		return sme.toString();
+	    }
+	    if (col == 1) {
+		return sme.getValue();
+	    }
+	    return null;
+	}
+	
+	public boolean isCellEditable(int row, int col) {
+	    if (col == 1) {
+		return true;
+	    }
+	    return false;
+	}
+
+	public void refresh() {
+	    search_metadata_entries = getEntries();
+	    
+	}
+	
+	public void setValueAt(Object new_value, int row, int col) {
+	    SearchMetadataEntry sme = (SearchMetadataEntry) search_metadata_entries.get(row);
+	    String old_value = sme.getValue();
+	    if (!new_value.equals(old_value)) {
+		sme.setValue((String)new_value);
+		
+	    }
+
+	}
+
+    }
+    private static class SearchMetadataTableCellRenderer
+	extends DefaultTableCellRenderer {
+
+	public void setValue(Object value) {
+	    
+	    setText((String)value);
+	}
+	/** Returns the default table cell renderer.
+	 * @param table The <strong>JTable</strong>.
+	 * @param value The value to assign to the cell at [row, column] as an <strong>Object</strong>.
+	 * @param isSelected <i>true</i> if cell is selected.
+	 * @param hasFocus <i>true</i> if cell has focus.
+	 * @param row The row of the cell to render as an <i>int</i>.
+	 * @param column The column of the cell to render as an <i>int</i>.
+	 * @return The default table cell renderer <strong>Component</strong>.
+	 */
+	public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
+	    JComponent component = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+
+	    // real_column is the column in the model, column is the column in the table - may be different if the user has moved the columns around
+	    int real_column =  table.convertColumnIndexToModel(column);
+	    if (real_column == 1 && isSelected) {
+		table.editCellAt(row, column);
+		if (table.isEditing()) {
+		    table.getEditorComponent().requestFocus();
+		}
+	    }
+	    
+	    // so we can see the background colour
+	    component.setOpaque(true);
+
+	    // Background
+	    if (isSelected) {
+		component.setBackground(Configuration.getColor("coloring.workspace_heading_background", true));
+	    }
+	    else {
+		if (real_column == 0) {
+		    component.setBackground(Configuration.getColor("coloring.collection_heading_background", true));
+		}
+		else {
+		    component.setBackground(Configuration.getColor("coloring.collection_tree_background", true));
+		}
+	    }
+	    
+	    // The value column of cells never paints focus
+	    if (real_column == 1) {
+		component.setBorder(BorderFactory.createEmptyBorder(0,2,0,0));
+	    }
+	 
+	    return component;
+	}
+
+    }
+    
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SharedByTwoFormatManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SharedByTwoFormatManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SharedByTwoFormatManager.java	(revision 31635)
@@ -0,0 +1,37 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+public interface SharedByTwoFormatManager {
+    public void destroy ();
+    public Control getControls ();
+    public void refresh ();
+    //public void modeChanged(int);
+    //public DOMProxyListEntry getCurrentFormat ();
+}
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Subcollection.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Subcollection.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/Subcollection.java	(revision 31635)
@@ -0,0 +1,270 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+
+/** This class encapsulates one subcollection entry in the collection configuration file.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */
+public class Subcollection
+    implements Comparable, DOMProxyListEntry {
+    /** A <i>boolean</i> which is <i>true</i> if the condition is an include one, <i>false</i> otherwise. */
+    private boolean include = true;
+    /** The DOM Element this Subcollection is based upon. */
+    private Element element = null;
+    /** A String containing a Perl expression which is used as the pattern by which to filter this subcollection. */
+    private String pattern = null;
+    /** A series of flags to be used when matching the expression. */
+    private String flags = null;
+    /** A String which is a unique identifier of a subcollection. */
+    private String name = null;
+    /** Either the fully qualified name of the metadata whose value should be matched against the given expression, or <i>null</i> if you wish to match against the file name. */
+    private String source = null;
+    private String text = null;
+
+    /** Default constructor which should only be used during DOMProxyListModel initialization. */
+    public Subcollection() {
+    }
+
+    /** Constructor for representing an existing assigned Subcollection.
+     * @param element the Element this subcollection is based upon
+     */
+    public Subcollection(Element element) {
+	this.element = element;
+    }
+
+    /** Constructor for assigning a brand new Subcollection.
+     * @param name a unique identifier for this collection as a String
+     * @param include true if this the pattern should be an inclusion filter, false for exclusion
+     * @param source either the fully qualified name of an element as a String, or null to filter filenames
+     * @param pattern the matching pattern as a String
+     * @param flags any flags to use while matching, as a String
+     */
+    public Subcollection(String name, boolean include, String source, String pattern, String flags) {
+	// Cache the details
+	this.flags = flags;
+	this.include = include;
+	this.name = name;
+	this.pattern = pattern;
+	if(source != null) {
+	    this.source = source;
+	}
+	else {
+	    this.source = StaticStrings.FILENAME_STR;
+	}
+	// Create a new DOM Element with the appropriate attributes and text value
+	element = CollectionConfiguration.createElement(StaticStrings.SUBCOLLECTION_ELEMENT);
+	element.setAttribute(StaticStrings.CONTENT_ATTRIBUTE, source);
+	element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+	element.setAttribute(StaticStrings.OPTIONS_ATTRIBUTE, flags);
+	element.setAttribute(StaticStrings.TYPE_ATTRIBUTE, (include ? StaticStrings.INCLUDE_STR : StaticStrings.EXCLUDE_STR));
+	XMLTools.setValue(element, pattern);
+    }
+
+    /** Method to compare two subcollections.
+     * @param object he other subcollection to compare to, as an Object
+     * @return an int which is &gt;0, 0, or &lt;0 if this subcollection is before, equal to, or after the target object respectively.
+     */
+    public int compareTo(Object object)
+    {
+	if (object == null) {
+	    return -1;
+	}
+
+	// Object may be a String
+	if (object instanceof String) {
+	    return getName().compareTo((String) object);
+	}
+
+	// Otherwise assume it is a Subcollection object
+	return getName().compareTo(((Subcollection) object).getName());
+    }
+
+
+    public DOMProxyListEntry create(Element element) {
+	return new Subcollection(element);
+    }
+
+    /** Method to check two subcollections for equality.
+     * @param object the other subcollection to compare to, as an Object
+     * @return true if the subcollections are equal, false otherwise.
+     */
+    public boolean equals(Object object) {
+	return (compareTo(object) == 0);
+    }
+
+    /** Retrieve the DOM Element this Subcollection is based upon.
+     * @return an Element
+     */
+    public Element getElement() {
+	return element;
+    }
+
+    /** Method to get the value of flags.
+     * @return the value of flags as a String
+     */
+    public String getFlags() {
+	if(flags == null && element != null) {
+	    flags = element.getAttribute(StaticStrings.OPTIONS_ATTRIBUTE);
+	}
+	return flags;
+    }
+
+    /** Method to get the value of name.
+     * @return String The value of name as a <strong>String</string>.
+     */
+    public String getName() {
+	if(name == null && element != null) {
+	    name = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+	}
+	return name;
+    }
+
+    public String getPattern() {
+	if(pattern == null && element != null) {
+	    pattern = XMLTools.getValue(element);
+	}
+	return pattern;
+    }
+
+    /** Method to get the name of the source of the strings used in pattern matching.
+     * @return a String which is either the fully qualified name of a metadata element, or filename
+     */
+    public String getSource()
+    {
+	if (source == null && element != null) {
+	    source = element.getAttribute(StaticStrings.CONTENT_ATTRIBUTE);
+	}
+
+	return source;
+    }
+
+    public boolean isAssigned() {
+	return (element != null && !element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR));
+    }
+
+    /** Method to get the value of include.
+     * @return boolean true if this is an inclusion filter, false otherwise
+     */
+    public boolean isInclusive() {
+	if (element != null) {
+	    include = element.getAttribute(StaticStrings.TYPE_ATTRIBUTE).equals(StaticStrings.INCLUDE_STR);
+	}
+
+	return include;
+    }
+
+    public void setAssigned(boolean assigned) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	}
+    }
+
+    public void setElement(Element element) {
+	this.element = element;
+	include = true;
+	flags = null;
+	name = null;
+	pattern = null;
+	source = null;
+	text = null;
+    }
+
+    public void setFlags(String new_flags) {
+	if(element != null) {
+	    // Change element
+	    element.setAttribute(StaticStrings.OPTIONS_ATTRIBUTE, new_flags);
+	    flags = new_flags;
+	    text = null;
+	}
+    }
+
+    public void setInclusive(boolean new_include) {
+	if(element != null) {
+	    // Change element
+	    include = new_include;
+	    element.setAttribute(StaticStrings.TYPE_ATTRIBUTE, (include ? StaticStrings.INCLUDE_STR : StaticStrings.EXCLUDE_STR));
+	    text = null;
+	}
+    }
+
+    public void setName(String new_name) {
+	if(element != null) {
+	    // Change element
+	    element.setAttribute(StaticStrings.NAME_ATTRIBUTE, new_name);
+	    name = new_name;
+	    text = null;
+	}
+    }
+
+    public void setPattern(String new_pattern) {
+	if(element != null) {
+	    // Change element
+	    XMLTools.setValue(element, new_pattern);
+	    pattern = new_pattern;
+	    text = null;
+	}
+    }
+
+    public void setSource(String new_source) {
+	if(element != null) {
+	    // Change element
+	    element.setAttribute(StaticStrings.CONTENT_ATTRIBUTE, new_source);
+	    source = new_source;
+	    text = null;
+	}
+    }
+
+    /** Method to display the contents of this class as it would appear in the collection configuration file.
+     * @return a String representing this subcollection
+     */
+    public String toString() {
+	if(text == null) {// && element != null) {
+
+	    StringBuffer new_text = new StringBuffer(getName());
+	    new_text.append(StaticStrings.SPACE_CHARACTER);
+	    new_text.append(StaticStrings.SPEECH_CHARACTER);
+	    if (!isInclusive()) {
+		new_text.append(StaticStrings.EXCLAMATION_CHARACTER);
+	    }
+	    new_text.append(getSource());
+	    new_text.append(StaticStrings.SEPARATOR_CHARACTER);
+	    new_text.append(getPattern());
+	    new_text.append(StaticStrings.SEPARATOR_CHARACTER);
+	    new_text.append(getFlags());
+	    new_text.append(StaticStrings.SPEECH_CHARACTER);
+	    text =  new_text.toString();
+	}
+	
+	return text;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SubcollectionIndex.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SubcollectionIndex.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SubcollectionIndex.java	(revision 31635)
@@ -0,0 +1,180 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.util.*;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+/** This class encapsulates a single indexing pair.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */
+public class SubcollectionIndex
+    implements Comparable, DOMProxyListEntry {
+
+    private ArrayList sources = null;
+    /** The element this index is based upon. */
+    private Element element = null;
+    /** The unique, if cryptic, identifier of an index. */
+    private String id = null;
+
+    /** Default constructor, which should only be used during DOMProxyListModel creation. */
+    public SubcollectionIndex() {
+    }
+
+    /** Constructor. */
+    public SubcollectionIndex(Element element) { 
+	this.element = element;
+    }
+
+    public SubcollectionIndex(Object[] raw_sources) {
+	this.sources = new ArrayList();
+	// Create a new element
+	element = CollectionConfiguration.createElement(StaticStrings.INDEX_ELEMENT);
+	// For each source add a content element
+	int size = raw_sources.length;
+	for(int i = 0; i < size; i++) {
+	    Subcollection subcollection = (Subcollection) raw_sources[i];
+	    String subcollection_name = subcollection.getName();
+	    sources.add(subcollection_name);
+	    Element content_element = CollectionConfiguration.createElement(StaticStrings.CONTENT_ELEMENT);
+	    content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, subcollection_name);
+	    element.appendChild(content_element);
+	    content_element = null;
+	}
+    }
+
+    /** Method to compare two indexes.
+     * @param object The other index as an <strong>Object</strong>.
+     * @return An <i>int</i> which indicates how the indexes compare.
+     * @see java.lang.String
+     */
+    public int compareTo(Object object) {
+	return getID().compareTo(((SubcollectionIndex)object).getID());
+    }
+    
+    public DOMProxyListEntry create(Element element) {      	
+	return new SubcollectionIndex(element);
+    }
+
+    /** Method to test for the equality of two indexes.
+     * @param object The other index as an <strong>Object</strong>.
+     * @return A <i>boolean</i> which is <i>true</i> if the two indexes are equal, <i>false</i> otherwise.
+     */
+    public boolean equals(Object object) {
+	return (compareTo(object) == 0);
+    }
+    
+    public Element getElement() {
+	return element;
+    }
+
+    public String getID() {
+	if(id == null) {
+	    StringBuffer id_buffer = new StringBuffer();
+	    // Write data information. Retrieve each of the content sources and add them in a comma separated list.
+	    ArrayList sources = getSources();
+	    int sources_size = sources.size();
+	    for(int i = 0; i < sources_size; i++) {
+		id_buffer.append((String)sources.get(i));
+		id_buffer.append(StaticStrings.COMMA_CHARACTER);
+	    }
+	    sources = null;
+	    id = id_buffer.substring(0, id_buffer.length() - 1);
+	}
+	return id;
+    }
+
+    /** Retrieve the sources of this index.
+     * @return the sources as an ArrayList
+     */
+    public ArrayList getSources() {
+	if(sources == null) {
+	    sources = new ArrayList();
+	    // for the last entry in the assigned subcollection partitions, element is null
+	    // why this is null for the last entry?
+	    if(element==null){	 
+	      return sources;
+	    } 
+	    NodeList content_elements = element.getElementsByTagName(StaticStrings.CONTENT_ELEMENT);	    
+	    int content_elements_length = content_elements.getLength();
+	    for(int i = 0; i < content_elements_length; i++) {
+		Element content_element = (Element) content_elements.item(i);
+		sources.add(content_element.getAttribute(StaticStrings.NAME_ATTRIBUTE));
+	    }
+	    content_elements = null;
+	    Collections.sort(sources);
+	}
+	return sources;
+    }
+
+    public boolean isAssigned() {
+	return (element != null && !element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR));
+    }
+
+    public void setAssigned(boolean assigned) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	}
+    }
+
+    public void setElement(Element element) {
+	this.element = element;
+	this.id = null;
+	this.sources = null;
+    }
+
+    /** Method to set the sources for this index which can only be used for the default index.
+     * @param sources an ArrayList of source names
+     */
+    public void setSources(ArrayList sources) {
+	if(element != null) {
+	    // Erase old sources
+	    XMLTools.clear(element);
+	    // For each entry in the sources array add a new content element.
+	    int size = sources.size();
+	    for(int i = 0; i < size; i++) {
+		Element content_element = element.getOwnerDocument().createElement(StaticStrings.CONTENT_ELEMENT);
+		content_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, (String) sources.get(i));
+		element.appendChild(content_element);
+		content_element = null;
+	    }
+	    this.id = null; // Regenerate ID.
+	    this.sources = sources;
+	}
+    }
+
+    /** Method to turn this object into a string representation ready to be placed in the collection configuration file.
+     * @return A <strong>String</strong> containing the information of this class.
+     */
+
+    public String toString() {
+	return getID();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SubcollectionIndexManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SubcollectionIndexManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SubcollectionIndexManager.java	(revision 31635)
@@ -0,0 +1,685 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.util.CheckList;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.*;
+
+
+/** This class maintains a list of indexes partitions for the purpose of defining subcollections.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */
+public class SubcollectionIndexManager
+extends DOMProxyListModel
+{
+	static final private Dimension FIELD_SIZE = new Dimension(200, 30);
+
+	private Control controls;
+	private DOMProxyListModel model;
+	private SubcollectionIndex default_index;
+
+
+	/** Constructor. */
+	public SubcollectionIndexManager(Element subindexes)
+	{
+		super(subindexes, StaticStrings.INDEX_ELEMENT, new SubcollectionIndex());
+		DebugStream.println("SubcollectionIndexManager: " + getSize() + " subcollection indexes parsed.");
+		this.model = this;
+
+		// Parse and retrieve the default index
+		NodeList default_index_elements = CollectionConfiguration.getElementsByTagName(StaticStrings.SUBCOLLECTION_DEFAULT_INDEX_ELEMENT);
+		if (default_index_elements.getLength() > 0) {
+			default_index = new SubcollectionIndex((Element) default_index_elements.item(0));
+		}
+	}
+
+
+	/** Method to add a subindex.
+	 * @param subindex a SubcollectionIndex
+	 * @see org.greenstone.gatherer.Gatherer
+	 * @see org.greenstone.gatherer.collection.CollectionManager
+	 */
+	private void addSubcollectionIndex(SubcollectionIndex subcollection_index)
+	{
+		if (!contains(subcollection_index)) {
+			// add a pseudo metadata
+			CollectionMeta metadatum = new CollectionMeta(StaticStrings.STOP_CHARACTER + subcollection_index.getID());
+			metadatum.setValue(subcollection_index.getID());
+			CollectionDesignManager.collectionmeta_manager.addMetadatum(metadatum);
+			add(getSize(), subcollection_index);
+		}
+	}
+
+
+	public void destroy()
+	{
+		if (controls != null) {
+			controls.destroy();
+			controls = null;
+		}
+		default_index = null;
+		model = null;
+	}
+
+
+	public Control getControls()
+	{
+		if (controls == null) {
+			controls = new SubcollectionIndexControls();
+		}
+		return controls;
+	}
+
+
+	/** Method to get all of the subindexes set.
+	 * @return an ArrayList containing all the defined indexes
+	 */
+	public ArrayList getSubcollectionIndexes()
+	{
+		return children();
+	}
+
+	/** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+	 * @param mode the new mode as an int
+	 */
+	public void modeChanged(int mode) {
+
+	}
+
+	private int moveSubcollectionIndex(SubcollectionIndex subcollection_index, boolean move_up)
+	{
+		// Determine the current position of the subcollection index
+		int position = indexOf(subcollection_index);
+		int new_position;
+
+		// Attempt to move the subcollection index up
+		if (move_up) {
+			// Check it's not already at the top
+			if (position == 0) {
+				return position;
+			}
+
+			// This automatically removes the index first, as an Element can only exist once in a particular document
+			new_position = position - 1;
+			addBefore(subcollection_index, (SubcollectionIndex) getElementAt(new_position));
+		}
+
+		// Attempt to move the subcollection index down
+		else {
+			// Check it's not already at the bottom
+			if (position == (getSize()) - 1) {
+				return position;
+			}
+
+			// This automatically removes the index first, as an Element can only exist once in a particular document
+			new_position = position + 1;
+			addAfter(subcollection_index, (SubcollectionIndex) getElementAt(new_position));
+		}
+
+		return new_position;
+	}
+
+
+	/** Method to remove a certain subcollection index. */
+	private void removeSubcollectionIndex(SubcollectionIndex subcollection_index)
+	{
+		if (subcollection_index != null) {
+			// Remove any current metadata from this index
+			CollectionDesignManager.collectionmeta_manager.removeMetadata(StaticStrings.STOP_CHARACTER + subcollection_index.getID());
+
+			// Check if the index removed happens to be the default index
+			if (default_index != null && default_index.equals(subcollection_index)) {
+				setDefault(null);
+			}
+
+			// Remove the index
+			remove(subcollection_index);
+		}
+	}
+
+
+	/** Method to remove all of the subindexes that contain a certain subcollection.
+	 * @param subcollection the Subcollection that has been removed
+	 * @see org.greenstone.gatherer.cdm.Subcollection
+	 * @see org.greenstone.gatherer.cdm.SubcollectionIndex
+	 */
+	public void removeSubcollectionIndexes(Subcollection subcollection)
+	{
+		String subcollection_name = subcollection.getName();
+		int size = getSize();
+		for(int i = size - 1; i >= 0; i--) {
+			SubcollectionIndex subcollection_index = (SubcollectionIndex) getElementAt(i);
+			if (subcollection_index.getSources().contains(subcollection_name)) {
+				removeSubcollectionIndex(subcollection_index);
+			}
+		}
+	}
+
+	private void replaceSubcollectionIndex(SubcollectionIndex old_subcollection, SubcollectionIndex new_subcollection) {
+		// Remove old collection meta
+		CollectionDesignManager.collectionmeta_manager.removeMetadata(StaticStrings.STOP_CHARACTER + old_subcollection.getID());
+		// Add new one
+		CollectionMeta metadatum = new CollectionMeta(StaticStrings.STOP_CHARACTER + new_subcollection.getID());
+		metadatum.setValue(new_subcollection.getID());
+		CollectionDesignManager.collectionmeta_manager.addMetadatum(metadatum);
+		if(default_index != null && default_index.equals(old_subcollection)) {
+			setDefault(new_subcollection);
+		}
+
+		// get the position of the old one
+		int position = indexOf(old_subcollection);
+		remove(old_subcollection);
+		add(position, new_subcollection);
+	}
+
+	/** Method to set the default subcollection index.
+	 * @param index The <strong>SubcollectionIndex</strong> to use as the default index.
+	 * @see org.greenstone.gatherer.Gatherer
+	 * @see org.greenstone.gatherer.collection.CollectionManager
+	 * @see org.greenstone.gatherer.cdm.SubcollectionIndex
+	 */
+	private void setDefault(SubcollectionIndex subcollection_index)
+	{
+		if (subcollection_index != null) {
+			if (default_index == null) {
+				// Create the default index element, and place immediately after indexes element.
+				Element default_index_element = root.getOwnerDocument().createElement(StaticStrings.SUBCOLLECTION_DEFAULT_INDEX_ELEMENT);
+				default_index = new SubcollectionIndex(default_index_element);
+				Node target_node = CollectionConfiguration.findInsertionPoint(default_index_element);
+				if (target_node != null) {
+					root.getOwnerDocument().getDocumentElement().insertBefore(default_index_element, target_node);
+				}
+				else {
+					root.getOwnerDocument().getDocumentElement().appendChild(default_index_element);
+				}
+			}
+			default_index.setAssigned(true);
+			default_index.setSources(subcollection_index.getSources());
+		}
+		else {
+			if (default_index != null) {
+				default_index.setAssigned(false);
+			}
+		}
+	}
+
+
+	/** This class creates a set of controls for editing the indexes. */
+	private class SubcollectionIndexControls
+	extends JPanel
+	implements Control
+	{
+		private CheckList source_list;
+		private JButton add_button;
+		private JButton move_down_button;
+		private JButton move_up_button;
+		private JButton remove_button;
+		private JButton replace_button;
+		private JButton select_all_button;
+		private JButton select_none_button; 
+		private JButton set_default_button;
+		private JList subcollection_index_list;
+		private JTextField subcollection_index_name_textfield;
+
+
+		/** Constructor.
+		 */
+		public SubcollectionIndexControls()
+		{
+			super();
+                        this.setComponentOrientation(Dictionary.getOrientation());   
+			ArrayList sources = new ArrayList();
+			ListModel source_model = CollectionDesignManager.subcollection_manager;
+			for (int i = 0; i < source_model.getSize(); i++) {
+				sources.add(source_model.getElementAt(i));
+			}
+
+			// Creation
+			JPanel assigned_indexes_pane = new JPanel();
+                        assigned_indexes_pane.setComponentOrientation(Dictionary.getOrientation());
+			JLabel index_label = new JLabel(Dictionary.get("CDM.SubcollectionIndexManager.Subindexes"));
+                        index_label.setComponentOrientation(Dictionary.getOrientation());
+			subcollection_index_list = new JList(model);
+			subcollection_index_list.setCellRenderer(new SubcollectionIndexListCellRenderer());
+			subcollection_index_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+			subcollection_index_list.setVisibleRowCount(2);
+                        subcollection_index_list.setComponentOrientation(Dictionary.getOrientation());
+                        
+			JPanel movement_pane = new JPanel();
+			movement_pane.setComponentOrientation(Dictionary.getOrientation());
+                        move_up_button = new GLIButton(Dictionary.get("CDM.Move.Move_Up"), JarTools.getImage("arrow-up.gif"), Dictionary.get("CDM.Move.Move_Up_Tooltip"));
+			move_up_button.setEnabled(false);
+
+			move_down_button = new GLIButton(Dictionary.get("CDM.Move.Move_Down"), JarTools.getImage("arrow-down.gif"), Dictionary.get("CDM.Move.Move_Down_Tooltip"));
+			move_down_button.setEnabled(false);
+
+			set_default_button = new GLIButton(Dictionary.get("CDM.SubcollectionIndexManager.Set_Default_Subindex"), Dictionary.get("CDM.SubcollectionIndexManager.Set_Default_Subindex_Tooltip"));
+			set_default_button.setEnabled(false);
+
+			JPanel index_pane = new JPanel();
+                        index_pane.setComponentOrientation(Dictionary.getOrientation());
+			JPanel details_pane = new JPanel();
+			details_pane.setComponentOrientation(Dictionary.getOrientation());
+			JPanel labels_pane = new JPanel();
+			labels_pane.setComponentOrientation(Dictionary.getOrientation());
+                        
+			select_all_button = new GLIButton(Dictionary.get("CDM.IndexManager.Select_All"), Dictionary.get("CDM.IndexManager.Select_All_Tooltip"));
+			select_none_button = new GLIButton(Dictionary.get("CDM.IndexManager.Select_None"), Dictionary.get("CDM.IndexManager.Select_None_Tooltip"));
+			
+			
+			JPanel boxes_pane = new JPanel();
+			boxes_pane.setComponentOrientation(Dictionary.getOrientation());
+                        JPanel content_pane = new JPanel();
+                        content_pane.setComponentOrientation(Dictionary.getOrientation());
+                        
+			JLabel source_label = new JLabel(Dictionary.get("CDM.SubcollectionIndexManager.Source"));
+                        source_label.setComponentOrientation(Dictionary.getOrientation());
+                        
+			source_list = new CheckList(false);
+			source_list.setListData(sources);
+			source_list.setToolTipText(Dictionary.get("CDM.SubcollectionIndexManager.Source_Tooltip"));
+			
+			select_all_button.setEnabled(isSelectAllEnabled());
+			select_none_button.setEnabled(isSelectAllEnabled());
+
+			JPanel button_pane = new JPanel();
+			button_pane.setComponentOrientation(Dictionary.getOrientation());
+                        
+                        add_button = new GLIButton(Dictionary.get("CDM.SubcollectionIndexManager.Add_Subindex"), Dictionary.get("CDM.SubcollectionIndexManager.Add_Subindex_Tooltip"));
+			add_button.setEnabled(false);
+
+			remove_button = new GLIButton(Dictionary.get("CDM.SubcollectionIndexManager.Remove_Subindex"), Dictionary.get("CDM.SubcollectionIndexManager.Remove_Subindex_Tooltip"));
+			remove_button.setEnabled(false);
+
+			replace_button = new GLIButton(Dictionary.get("CDM.SubcollectionIndexManager.Replace_Subindex"), Dictionary.get("CDM.SubcollectionIndexManager.Replace_Subindex_Tooltip"));
+			replace_button.setEnabled(false);
+						
+
+			// Listeners
+			add_button.addActionListener(new AddListener());
+			add_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+			move_down_button.addActionListener(new MoveListener(false));
+			move_down_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+			move_up_button.addActionListener(new MoveListener(true));
+			move_up_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+			remove_button.addActionListener(new RemoveListener());
+			remove_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+			replace_button.addActionListener(new ReplaceListener());
+			replace_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+			select_all_button.addActionListener(new SelectAllListener());			
+			select_all_button.addActionListener(CollectionDesignManager.buildcol_change_listener);			
+			select_none_button.addActionListener(new SelectNoneListener());
+			select_none_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+			set_default_button.addActionListener(new SetDefaultListener());
+			set_default_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+			subcollection_index_list.addListSelectionListener(new AssignedListListener());
+			source_list.addListSelectionListener(new SourceListListener());		
+			
+			
+			// Layout
+			movement_pane.setBorder(BorderFactory.createEmptyBorder(0,2,0,0));
+			movement_pane.setLayout(new GridLayout(3,1));
+			movement_pane.add(move_up_button);
+			movement_pane.add(move_down_button);
+			movement_pane.add(set_default_button);
+
+			assigned_indexes_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+			assigned_indexes_pane.setLayout(new BorderLayout());
+			assigned_indexes_pane.add(index_label, BorderLayout.NORTH);
+			assigned_indexes_pane.add(new JScrollPane(subcollection_index_list), BorderLayout.CENTER);
+			assigned_indexes_pane.add(movement_pane, BorderLayout.LINE_END);
+
+			labels_pane.setLayout(new BorderLayout());
+			labels_pane.setBorder(BorderFactory.createEmptyBorder(5, 5, 10, 5));
+			
+			// add the select all and select none buttons to the label panel			 
+			GridBagLayout gridbag = new GridBagLayout();			
+			GridBagConstraints c = new GridBagConstraints();
+			labels_pane.setLayout(gridbag);
+			
+			c.fill = GridBagConstraints.BOTH;		
+			c.weightx = 1.0;
+			c.weighty = 1.0;
+			c.gridx = 0;
+			c.gridy = 0;
+			
+			gridbag.setConstraints(source_label, c);
+			labels_pane.add(source_label);
+			
+			c.fill = GridBagConstraints.HORIZONTAL;
+			c.weighty = 0.0;
+			c.gridy = 1;
+			
+			gridbag.setConstraints(select_all_button, c);
+			labels_pane.add(select_all_button);
+			
+			c.gridy = 2;
+			gridbag.setConstraints(select_none_button, c);
+			labels_pane.add(select_none_button);	
+			
+			
+			boxes_pane.setLayout(new BorderLayout());
+			boxes_pane.add(new JScrollPane(source_list), BorderLayout.CENTER);
+
+			details_pane.setLayout(new BorderLayout());
+			details_pane.add(labels_pane, BorderLayout.LINE_START);
+			details_pane.add(boxes_pane, BorderLayout.CENTER);
+
+			button_pane.setLayout(new GridLayout(1,3));
+			button_pane.add(add_button);
+			button_pane.add(replace_button);
+			button_pane.add(remove_button);
+
+			index_pane.setLayout(new BorderLayout());
+			index_pane.add(details_pane, BorderLayout.CENTER);
+			index_pane.add(button_pane, BorderLayout.SOUTH);
+
+			content_pane.setLayout(new BorderLayout());
+			content_pane.add(assigned_indexes_pane, BorderLayout.NORTH);
+			content_pane.add(index_pane, BorderLayout.CENTER);
+
+			setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+			setLayout(new BorderLayout());
+			add(content_pane, BorderLayout.CENTER);
+		}
+
+
+		public void destroy()
+		{
+		}
+
+
+		public void gainFocus()
+		{
+			// Reload the source list
+			ArrayList sources = new ArrayList();
+			ListModel source_model = CollectionDesignManager.subcollection_manager;
+			for (int i = 0; i < source_model.getSize(); i++) {
+				sources.add(source_model.getElementAt(i));
+			}
+			source_list.setListData(sources);
+
+			// Refresh the subcollection index list
+			subcollection_index_list.updateUI();
+
+			clearControls();
+
+		}
+
+
+		public void loseFocus() {
+		}
+
+		private void clearControls() {
+			subcollection_index_list.clearSelection();
+			source_list.clearTicked();
+			add_button.setEnabled(false);
+			remove_button.setEnabled(false);
+			replace_button.setEnabled(false);
+			set_default_button.setEnabled(false);
+			move_down_button.setEnabled(false);
+			move_up_button.setEnabled(false);
+			select_all_button.setEnabled(isSelectAllEnabled());			
+			select_none_button.setEnabled(isSelectAllEnabled());
+		}
+		
+		private boolean isSelectAllEnabled(){
+		  	return source_list.getModel().getSize() > 0 ? true : false;		  	
+		}
+
+		private void updateControlsWithSelectedIndex()
+		{
+			SubcollectionIndex selected_index = (SubcollectionIndex) subcollection_index_list.getSelectedValue();
+			if (selected_index == null) {
+				source_list.clearTicked();
+				return;
+			}
+
+			// Display the selected subcollection index's sources
+			source_list.clearTicked();
+			source_list.setTickedObjects(selected_index.getSources().toArray());
+		}
+
+
+		private void validateButtons() {
+
+			boolean add_enabled = false;
+			boolean replace_enabled = false;
+
+			// Can't add a new index if no sources are selected
+			if (!source_list.isNothingTicked()) {
+				ArrayList sources = source_list.getTicked();
+				SubcollectionIndex subcollection_index = new SubcollectionIndex(sources.toArray());
+
+				// Subcollection index already exists: can't add, but can replace if the name has changed
+				if (!model.contains(subcollection_index)) {
+					add_enabled = true;
+					if (!subcollection_index_list.isSelectionEmpty()) {
+						replace_enabled = true;
+					}
+				}
+			}
+			
+			select_all_button.setEnabled(isSelectAllEnabled());
+			select_none_button.setEnabled(isSelectAllEnabled());
+
+			// We should now know the add_button state
+			add_button.setEnabled(add_enabled);
+			replace_button.setEnabled(replace_enabled);
+		}
+
+
+		private class AddListener
+		implements ActionListener {
+		  
+			public void actionPerformed(ActionEvent event)
+			{
+				if (!source_list.isNothingTicked()) {
+					ArrayList sources = source_list.getTicked();
+					SubcollectionIndex subcollection_index = new SubcollectionIndex(sources.toArray());
+					addSubcollectionIndex(subcollection_index);
+					clearControls();
+				}
+			}
+		}
+
+
+		private class MoveListener
+		implements ActionListener
+		{
+			private boolean move_up;
+
+			public MoveListener(boolean move_up)
+			{
+				this.move_up = move_up;
+			}
+
+			public void actionPerformed(ActionEvent event)
+			{
+				// Retrieve the selected subcollection index
+				SubcollectionIndex subcollection_index = (SubcollectionIndex) subcollection_index_list.getSelectedValue();
+				if (subcollection_index != null) {
+					int new_position = moveSubcollectionIndex(subcollection_index, move_up);
+					// Ensure the subcollection index that moved is still selected
+					subcollection_index_list.setSelectedIndex(new_position);					
+				}
+			}
+		}
+
+		private class RemoveListener
+		implements ActionListener
+		{
+			public void actionPerformed(ActionEvent event) {
+
+				SubcollectionIndex delete_me = (SubcollectionIndex) subcollection_index_list.getSelectedValue();
+				if (delete_me != null) {
+					removeSubcollectionIndex(delete_me);
+				}
+			}
+		}
+
+
+		private class ReplaceListener
+		implements ActionListener {
+
+			public void actionPerformed(ActionEvent event) {
+
+				if (subcollection_index_list.isSelectionEmpty()|| source_list.isNothingTicked()) {
+					// This should never happen, but just in case...
+					replace_button.setEnabled(false);
+					return;
+				}
+				SubcollectionIndex old_index = (SubcollectionIndex) subcollection_index_list.getSelectedValue();
+				ArrayList sources = source_list.getTicked();
+				SubcollectionIndex new_index = new SubcollectionIndex(sources.toArray());
+				replaceSubcollectionIndex(old_index, new_index);
+			}
+		}
+
+
+		private class SetDefaultListener
+		implements ActionListener {
+
+			public void actionPerformed(ActionEvent event)
+			{
+				SubcollectionIndex subcollection_index = (SubcollectionIndex) subcollection_index_list.getSelectedValue();
+				if (subcollection_index != null) {
+					setDefault(subcollection_index);
+					// This should cause a repaint of just the desired row
+					subcollection_index_list.setSelectedValue(subcollection_index, true);
+				}
+				set_default_button.setEnabled(false);
+			}
+		}
+
+
+		private class SourceListListener
+		implements ListSelectionListener {
+
+			public void valueChanged(ListSelectionEvent event) {
+				if (event.getValueIsAdjusting()) {
+					return;
+				}
+				validateButtons();
+			}
+		}
+		
+		private class SelectAllListener
+		implements ActionListener {
+			
+			public void actionPerformed(ActionEvent event){
+				if(select_all_button.isEnabled()){
+					source_list.setAllTicked();
+					validateButtons();
+				}
+			}
+		}
+		
+		private class SelectNoneListener
+		implements ActionListener {
+			
+			public void actionPerformed(ActionEvent event){
+				if(select_none_button.isEnabled()){
+					source_list.clearTicked();
+					validateButtons();
+				}
+			}
+		}
+
+
+		/** Listens for selections within the assigned subcollection indexes list  */
+		private class AssignedListListener
+		implements ListSelectionListener {
+
+			public void valueChanged(ListSelectionEvent event) {
+
+				if (event.getValueIsAdjusting()) {
+					return;
+				}
+
+				if (subcollection_index_list.isSelectionEmpty()) {
+					clearControls();
+					return;
+				}
+
+				int i = subcollection_index_list.getSelectedIndex();
+				int size = subcollection_index_list.getModel().getSize();
+				
+				select_all_button.setEnabled(isSelectAllEnabled());
+				select_none_button.setEnabled(isSelectAllEnabled());
+
+				// Enable the buttons appropriately
+				remove_button.setEnabled(true);
+				replace_button.setEnabled(false);
+				add_button.setEnabled(false);
+				SubcollectionIndex selected_index = (SubcollectionIndex) subcollection_index_list.getSelectedValue();
+				set_default_button.setEnabled(default_index == null || !default_index.equals(selected_index));
+				if (i > 0) {
+					move_up_button.setEnabled(true);					
+				}
+				else {
+					move_up_button.setEnabled(false);
+				}
+				if (i < size-1){
+					move_down_button.setEnabled(true);
+				}
+				else {
+					move_down_button.setEnabled(false);
+				}
+
+				// Need to fill in the rest of the bits
+				updateControlsWithSelectedIndex();
+			}
+		}
+
+
+		private class SubcollectionIndexListCellRenderer
+		extends DefaultListCellRenderer
+		{
+			/** Return a component that has been configured to display the specified value. */
+			public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+				JLabel component = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+				if (default_index != null && default_index.equals(value)) {
+					component.setText(component.getText() + " " + Dictionary.get("CDM.SubcollectionIndexManager.Default_Partition_Indicator"));
+				}
+				return component;
+			}
+		}
+	}
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SubcollectionManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SubcollectionManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SubcollectionManager.java	(revision 31635)
@@ -0,0 +1,515 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.NonWhitespaceField;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.*;
+
+/** This class maintains a list of subcollections within our collection.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */
+public class SubcollectionManager
+    extends DOMProxyListModel {
+
+    static final private String DISABLED_CONTROLS = "Disabled";
+    static final private String ENABLED_CONTROLS  = "Normal";
+    static final private String CLASS_DICTIONARY_NAME = "CDM.SubcollectionManager.";
+
+    /** The controls used to edit the settings of this manager. */
+    private Control controls = null;
+
+    private DOMProxyListModel model;
+
+    /** Constructor. 
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.cdm.CollectionConfiguration
+     * @see org.greenstone.gatherer.cdm.CollectionDesignManager
+     * @see org.greenstone.gatherer.cdm.DOMProxyListModel
+     * @see org.greenstone.gatherer.cdm.Subcollection
+     */
+    public SubcollectionManager() {
+	super(CollectionDesignManager.collect_config.getDocumentElement(), StaticStrings.SUBCOLLECTION_ELEMENT, new Subcollection());
+	DebugStream.println("SubcollectionManager: " + getSize() + " subcollections parsed.");
+	this.model = this;
+    }
+
+    /** Method to add a new subcollection.
+     * @param subcollection the Subcollection to add
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.cdm.CollectionConfiguration
+     * @see org.greenstone.gatherer.cdm.DOMProxyListModel
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     */
+    private void addSubcollection(Subcollection subcollection) {
+	if(!contains(subcollection)) {
+	    Element element = subcollection.getElement();
+	    // Locate where we should insert this new subcollection. 
+	    Node target_node = CollectionConfiguration.findInsertionPoint(element);
+	    // Failing that we insert immediately after a language string
+	    add(root, subcollection, target_node);
+	}
+    }
+
+    public void destroy() {
+	if(controls != null) {
+	    controls.destroy();
+	    controls = null;
+	}
+    }
+
+    /** Method to retrieve the controls for this manager.
+     * @return the Control used to edit the subcollection data
+     */
+    public Control getControls() {
+	if(controls == null) {
+	    controls = new SubcollectionControl();
+	}
+	return controls;
+    }
+
+    /** Method to retrieve a certain subcollection by its name.
+     * @param name a String which is used as the key for finding the matching subcollection
+     * @return the requested Subcollection or null if no such subcollection exists.
+     */
+    private Subcollection getSubcollection(String name) {
+	Subcollection result = null;
+	int size = getSize();
+	for(int i = 0; i < size; i++) {
+	    Subcollection subcollection = (Subcollection) getElementAt(i);
+	    if(subcollection.getName().equals(name)) {
+		result = subcollection;
+	    }
+	}
+	return result;
+    }
+
+
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+     public void modeChanged(int mode) {
+     }    
+
+    /** Method to remove the given subcollection.
+     * @param subcollection the Subcollection you want to remove
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     */
+    private void removeSubcollection(Subcollection subcollection) {
+	remove(subcollection);
+    }
+
+    private void updateSubcollection(Subcollection subcollection, String name, boolean include, String source, String pattern, String flags) {
+	subcollection.setFlags(flags);
+	subcollection.setInclusive(include);
+	subcollection.setName(name);
+	subcollection.setPattern(pattern);
+	subcollection.setSource(source);
+	refresh(subcollection);
+    }
+
+    /** This class creates a JPanel containing serveral more controls used for editing subcollection information. */
+    private class SubcollectionControl
+	extends JPanel 
+	implements Control, ChangeListener
+    {
+	private JButton add_button;
+	private JButton remove_button;
+	private JButton update_button;
+	private JComboBox source_combobox;
+	private JList subcollection_list;
+	private JTabbedPane tabbed_pane;
+	private JTextField flags_field;
+	private JTextField match_field;
+	private JTextField name_field;
+	private JRadioButton exclude_button;
+	private JRadioButton include_button;
+
+	/** Constructor */
+	public SubcollectionControl() {
+	    // Create
+	    JPanel header_pane = new DesignPaneHeader("CDM.GUI.Subcollections", "partitionindexes");
+
+	    tabbed_pane = new JTabbedPane();
+	    tabbed_pane.addChangeListener(this);
+            tabbed_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel button_pane_3 = new JPanel();
+	    button_pane_3.setComponentOrientation(Dictionary.getOrientation());
+            add_button = new GLIButton(Dictionary.get("CDM.SubcollectionManager.Add"), Dictionary.get("CDM.SubcollectionManager.Add_Tooltip"));
+	    add_button.setEnabled(false);
+	    
+	    remove_button = new GLIButton(Dictionary.get("CDM.SubcollectionManager.Remove"), Dictionary.get("CDM.SubcollectionManager.Remove_Tooltip"));
+	    remove_button.setEnabled(false);
+	    
+	    update_button = new GLIButton(Dictionary.get("CDM.SubcollectionManager.Replace"), Dictionary.get("CDM.SubcollectionManager.Replace_Tooltip"));
+	    update_button.setEnabled(false);
+	    
+	    JPanel button_pane = new JPanel();
+            button_pane.setComponentOrientation(Dictionary.getOrientation());
+	    JPanel button_pane_1 = new JPanel();
+            button_pane_1.setComponentOrientation(Dictionary.getOrientation());
+	    include_button = new JRadioButton(Dictionary.get("CDM.SubcollectionManager.Include"));
+	    include_button.setComponentOrientation(Dictionary.getOrientation());
+            include_button.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    include_button.setOpaque(false);
+	    
+	    exclude_button = new JRadioButton(Dictionary.get("CDM.SubcollectionManager.Exclude"));
+	    exclude_button.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    exclude_button.setOpaque(false);
+	    exclude_button.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JLabel flags_label = new JLabel(Dictionary.get("CDM.SubcollectionManager.Flags"));
+	    flags_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    flags_field = new NonWhitespaceField();
+	    flags_field.setToolTipText(Dictionary.get("CDM.SubcollectionManager.Flags_Tooltip"));
+            flags_field.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel inclusive_pane = new JPanel();
+            inclusive_pane.setComponentOrientation(Dictionary.getOrientation());
+	    JLabel inclusive_label = new JLabel(Dictionary.get("CDM.SubcollectionManager.Inclusive"));
+	    inclusive_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JLabel match_label = new JLabel(Dictionary.get("CDM.SubcollectionManager.Match"));
+	    match_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    match_field = new JTextField();
+	    match_field.setToolTipText(Dictionary.get("CDM.SubcollectionManager.Match_Tooltip"));
+	    match_field.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JLabel name_label = new JLabel(Dictionary.get("CDM.SubcollectionManager.Name"));
+	    name_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    name_field = new NonWhitespaceField();
+	    name_field.setToolTipText(Dictionary.get("CDM.SubcollectionManager.Name_Tooltip"));
+	    
+	    JLabel source_label = new JLabel(Dictionary.get("CDM.SubcollectionManager.Source"));
+	    source_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    ArrayList every_metadata_set_element = MetadataSetManager.getEveryMetadataSetElement();
+	    Vector source_model = new Vector(every_metadata_set_element);
+	    source_model.add(0, StaticStrings.FILENAME_STR);
+	    source_combobox = new JComboBox(source_model);
+            source_combobox.setComponentOrientation(Dictionary.getOrientation());
+	    source_combobox.setOpaque(false);
+	    source_combobox.setToolTipText(Dictionary.get("CDM.SubcollectionManager.Source_Tooltip"));
+	    
+	    subcollection_list = new JList(model);
+	    subcollection_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+	    JPanel subcollection_pane = new JPanel();
+            subcollection_pane.setComponentOrientation(Dictionary.getOrientation());
+	    ButtonGroup bg = new ButtonGroup();
+	    bg.add(include_button);
+	    bg.add(exclude_button);
+	    include_button.setSelected(true);
+	    JPanel subcollection_list_pane = new JPanel();
+            subcollection_list_pane.setComponentOrientation(Dictionary.getOrientation());
+	    JLabel subcollection_list_label = new JLabel(Dictionary.get("CDM.SubcollectionManager.Assigned"));
+	    subcollection_list_label.setComponentOrientation(Dictionary.getOrientation());
+
+	    // Add listeners
+	    SubCollectionChangeListener cl = new SubCollectionChangeListener();
+	    add_button.addActionListener(new AddSubCollectionListener());
+	    add_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    remove_button.addActionListener(new RemoveSubCollectionListener());
+	    remove_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    update_button.addActionListener(new UpdateSubCollectionListener());
+	    update_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
+	    exclude_button.addActionListener(cl);
+	    include_button.addActionListener(cl);
+	    source_combobox.addActionListener(cl);
+	    flags_field.getDocument().addDocumentListener(cl);
+	    match_field.getDocument().addDocumentListener(cl);
+	    name_field.getDocument().addDocumentListener(cl);
+	    subcollection_list.addListSelectionListener(new SubCollectionListListener());
+
+	    inclusive_pane.setLayout(new GridLayout());
+	    inclusive_pane.add(include_button);
+	    inclusive_pane.add(exclude_button);
+
+	    button_pane_1.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    button_pane_1.setLayout(new GridLayout(5, 2));
+	    button_pane_1.add(name_label);
+	    button_pane_1.add(name_field);
+	    button_pane_1.add(source_label);
+	    button_pane_1.add(source_combobox);
+	    button_pane_1.add(match_label);
+	    button_pane_1.add(match_field);
+	    button_pane_1.add(inclusive_label);
+	    button_pane_1.add(inclusive_pane);
+	    button_pane_1.add(flags_label);
+	    button_pane_1.add(flags_field);
+
+	    button_pane_3.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    button_pane_3.setLayout(new GridLayout(1,3));
+	    button_pane_3.add(add_button);
+	    button_pane_3.add(update_button);
+	    button_pane_3.add(remove_button);
+
+	    button_pane.setLayout(new BorderLayout());
+	    button_pane.add(button_pane_1, BorderLayout.CENTER);
+	    button_pane.add(button_pane_3, BorderLayout.SOUTH);
+
+	    subcollection_list_pane.setLayout(new BorderLayout());
+	    subcollection_list_pane.add(subcollection_list_label, BorderLayout.NORTH);
+	    subcollection_list_pane.add(new JScrollPane(subcollection_list), BorderLayout.CENTER);
+	    subcollection_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    subcollection_pane.setLayout(new BorderLayout());
+	    subcollection_pane.add(subcollection_list_pane, BorderLayout.CENTER);
+	    subcollection_pane.add(button_pane, BorderLayout.SOUTH);
+
+	    tabbed_pane.addTab(Dictionary.get("CDM.SubcollectionManager.Subcollection_Controls"), subcollection_pane);
+	    tabbed_pane.addTab(Dictionary.get("CDM.SubcollectionManager.Subindex_Controls"), (JPanel) CollectionDesignManager.subcollectionindex_manager.getControls());
+	    tabbed_pane.addTab(Dictionary.get("CDM.SubcollectionManager.Language_Controls"), (JPanel) CollectionDesignManager.language_manager.getControls());
+
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(header_pane, BorderLayout.NORTH);
+	    add(tabbed_pane, BorderLayout.CENTER);
+	}
+       	
+	/** Method to unregister any listeners to avoid memory leaks.
+	 */
+	public void destroy() {
+	}
+
+	public void gainFocus() {
+	    // Rebuild the sources combobox
+	    ArrayList every_metadata_set_element = MetadataSetManager.getEveryMetadataSetElement();
+	    Vector source_model = new Vector(every_metadata_set_element);
+	    source_model.add(0, "Filename"); // Add filename as a possible source.
+	    source_combobox.setModel(new DefaultComboBoxModel(source_model));
+	}
+
+	public void loseFocus() {
+	}
+	
+	public void stateChanged(ChangeEvent event)
+	{
+	    if (tabbed_pane.getSelectedIndex() == 1) {
+		CollectionDesignManager.subcollectionindex_manager.getControls().gainFocus();
+	    }
+	    if (tabbed_pane.getSelectedIndex() == 2) {
+		CollectionDesignManager.language_manager.getControls().gainFocus();
+	    }
+	}
+
+	/** Listens for actions apon the 'add' button in the SubcollectionManager controls, and if detected calls the addSubcollection method of the manager with a newly created subcollection. */
+	private class AddSubCollectionListener
+	    implements ActionListener {
+	    /** Any implementation of ActionListener must include this method so we can be informed when an action has been performed on one of our target controls. In this case we wish to retrieve information from the various edit controls, and if we have sufficient data to build a new subcollection do so.
+	     * @param event An <strong>ActionEvent</strong> containing information about the event.
+	     * @see org.greenstone.gatherer.cdm.Subcollection
+	     */
+	    public void actionPerformed(ActionEvent event) {
+		String name = name_field.getText();
+		String source = null;
+		Object object = source_combobox.getSelectedItem();
+		if (object instanceof MetadataElement) {
+		    MetadataElement metadata_element = (MetadataElement) object;
+		    source = metadata_element.getFullName();
+		}
+		else {
+		    source = object.toString();
+		}
+		String pattern = match_field.getText();
+		String flags = flags_field.getText();
+		if(name.length() > 0 && (source == null || source.length() > 0) && pattern.length() > 0) {
+		    Subcollection subcollection = new Subcollection(name, include_button.isSelected(), source, pattern, flags);
+		    addSubcollection(subcollection);
+		    // Change the selection to the new subcollection
+		    subcollection_list.setSelectedValue(subcollection, true);
+		}
+		add_button.setEnabled(false);
+	    }
+	}
+
+	/** This class listens for any key entry in a text field, selection change in a combobox or button click, and updates a subcollection as appropriate. Its also convenient to use this class to test if the add button should be active yet. */
+	private class SubCollectionChangeListener
+	    implements DocumentListener, ActionListener {
+	    /** Any implementation of ActionListener must include this method so we can be informed when an action has been performed on one of our target controls. In this case we want to record that somethings changed, then validate the controls.
+	     * @param event An <strong>ActionEvent</strong> containing information about the event.
+	     */
+	    public void actionPerformed(ActionEvent event) {
+		validateAdd();
+	    }
+
+	    public void changedUpdate(DocumentEvent event) {
+		validateAdd();
+	    }
+
+	    public void insertUpdate(DocumentEvent event) {
+		validateAdd();
+
+	    }
+
+	    public void removeUpdate(DocumentEvent event) {
+		validateAdd();
+	    }
+
+	    /** Method to validate the current subcollection editor values, and enable or disable controls (add button) based on said values. */
+	    private void validateAdd() {
+		if(name_field.getText().length() > 0 && match_field.getText().length() > 0) {
+		    if (getSubcollection(name_field.getText()) == null) {
+			add_button.setEnabled(true);
+		    } else {
+			add_button.setEnabled(false);
+		    }
+		}
+		else {
+		    add_button.setEnabled(false);
+		}
+	    }
+	}
+
+	/** Listens for actions apon the 'remove' button in the SubcollectionManager controls, and if detected calls the remove method of the manager with the SubIndex selected for removal. */
+	private class RemoveSubCollectionListener
+	    implements ActionListener {
+	    /** Any implementation of ActionListener must include this method so we can be informed when an action has been performed on one of our target controls. In this case we want to check if they have a subcolleciton selected, and if so remove both it and any subindexes based on it.
+	     * @param event An <strong>ActionEvent</strong> containing information about the event.
+	     * @see org.greenstone.gatherer.cdm.Subcollection
+	     */
+	    public void actionPerformed(ActionEvent event) {
+		if(!subcollection_list.isSelectionEmpty()) {
+		    Subcollection subcollection = (Subcollection)subcollection_list.getSelectedValue();
+		    removeSubcollection(subcollection);
+		    // And remove subcollection indexes dependant on this subcollection
+		    CollectionDesignManager.subcollectionindex_manager.removeSubcollectionIndexes(subcollection);
+		}
+		remove_button.setEnabled(false);
+	    }
+	}
+
+	private class UpdateSubCollectionListener
+	    implements ActionListener {
+	    public void actionPerformed(ActionEvent event) {
+		if(!subcollection_list.isSelectionEmpty()) {
+		    Subcollection subcollection = (Subcollection)subcollection_list.getSelectedValue();
+		    String name = name_field.getText();
+		    String source = null;
+		    Object object = source_combobox.getSelectedItem();
+		    if (object instanceof MetadataElement) {
+			MetadataElement metadata_element = (MetadataElement) object;
+			source = metadata_element.getFullName();
+		    }
+		    else {
+			source = object.toString();
+		    }
+		    String pattern = match_field.getText();
+		    String flags = flags_field.getText();
+		    if(name.length() > 0 && (source == null || source.length() > 0) && pattern.length() > 0) {
+			updateSubcollection(subcollection, name, include_button.isSelected(), source, pattern, flags);
+		    }
+		}
+	    }
+	}
+
+	/** This class listens for selections in the list on the subcollections pane of the SubcollectionManager, and updates the controls as necessary to reflect selection. */
+	private class SubCollectionListListener
+	    implements ListSelectionListener {
+	    /** Any implementation of ListSelectionListener must include this method so we can be informed when the selection changes. In this case we want to execute any changes the users made to the entry, then update the controls with details of the new selection.
+	     * @param event A <strong>ListSelectionEvent</strong> containing information related to this event.
+	     * @see org.greenstone.gatherer.cdm.Subcollection
+	     */
+	    public void valueChanged(ListSelectionEvent event) {
+		// Wait until the event stabilises to avoid processing it multiple times
+		if (event.getValueIsAdjusting() == true) {
+		    return;
+		}
+		// Now the entry
+		if(!subcollection_list.isSelectionEmpty()) {
+		    Subcollection subcollection = (Subcollection) subcollection_list.getSelectedValue();
+		    flags_field.setText(subcollection.getFlags());
+		    include_button.setSelected(subcollection.isInclusive());
+		    exclude_button.setSelected(!subcollection.isInclusive());
+		    match_field.setText(subcollection.getPattern());
+		    name_field.setText(subcollection.getName());
+		    String s = subcollection.getSource();
+		    int pos = 0;
+		    Object value = source_combobox.getItemAt(pos);
+		    //ystem.err.println("Search for: " + s);
+		    while (value != null) {
+			if (value instanceof MetadataElement) {
+			    MetadataElement metadata_element = (MetadataElement) value;
+			    String metadata_element_name = metadata_element.getFullName();
+			    //ystem.err.print("Compare to: " + e_name);
+			    if (metadata_element_name.equals(s)) {
+				source_combobox.setSelectedIndex(pos);
+				value = null;
+				//ystem.err.println(" - Match");
+			    }
+			    else {
+				pos++;
+				value = source_combobox.getItemAt(pos);
+				//ystem.err.println(" - Fail");
+			    }
+			}
+			else if(value.toString().equals(s)) {
+			    source_combobox.setSelectedIndex(pos);
+			    value = null;
+			}
+			else {
+			    pos++;
+			    value = source_combobox.getItemAt(pos);
+			}
+		    }
+		    // Can't add one thats already there.
+		    add_button.setEnabled(false);
+		    // You can update or remove it though...
+		    remove_button.setEnabled(true);
+		    update_button.setEnabled(true);
+		}
+		else {
+		    flags_field.setText("");
+		    include_button.setSelected(true);
+		    match_field.setText("");
+		    name_field.setText("");
+		    source_combobox.setSelectedIndex(0);
+		    remove_button.setEnabled(false);
+		    update_button.setEnabled(false);
+		}
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SuperCollection.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SuperCollection.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SuperCollection.java	(revision 31635)
@@ -0,0 +1,97 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.Element;
+
+
+public class SuperCollection
+    implements DOMProxyListEntry {
+
+    private Element element;
+    private String name;
+
+    public SuperCollection() {
+	element = null;
+	name = null;
+    }
+
+    public SuperCollection(Element element) {
+	this.element = element;
+	name = null;
+    }
+
+    public int compareTo(Object other) {
+	return this.getName().compareTo(other.toString());
+    }
+
+    public DOMProxyListEntry create(Element element) {
+	return new SuperCollection(element);
+    } 
+
+    public boolean equals(Object other) {
+	return (compareTo(other) == 0);
+    }
+
+    public Element getElement() {
+	return element;
+    }
+
+    public String getName() {
+	if(name == null && element != null) {
+	    name = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+	}
+	return name;
+    }
+
+    public boolean isAssigned() {
+	return (element != null && element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.TRUE_STR));
+    }
+
+    public void setAssigned(boolean assigned) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	}
+    }
+
+    public void setElement(Element element) {
+	this.element = element;
+	name = null;
+    }
+
+    public void setName(String name) {
+	this.name = name;
+	if(element != null) {
+	    element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+	}
+    }
+
+    public String toString() {
+	return getName();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SuperCollectionManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SuperCollectionManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/SuperCollectionManager.java	(revision 31635)
@@ -0,0 +1,255 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.BasicCollectionConfiguration;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.util.CheckList;
+import org.greenstone.gatherer.util.CheckListEntry;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.Dictionary;
+import org.w3c.dom.*;
+
+/** This class contains the information about what supercollection has been specified (if any) and methods for changing this information. Note that there is a major difference between this manager and the others in that its DOM model is never used directly in any list component. It is only used to decide whether a certain entry in the actual checklist is checked. */
+public class SuperCollectionManager
+    extends DOMProxyListModel {
+
+    static final public String SUPERCOLLECTION_COMMAND = "supercollection";
+    static final public String CCS_COMMAND = "ccs";
+    private ArrayList collection_checklist_model = null; // Model used to actually populate list
+    private Control controls = null;
+    private DOMProxyListModel model = null;
+    private String current_coll_name = null;
+    private boolean superCollectionChanged = false;
+
+    public SuperCollectionManager(Element supercollections_element) {
+	super(supercollections_element, StaticStrings.COLLECTION_ELEMENT, new SuperCollection());
+	DebugStream.println("SuperCollectionManager: " + getSize() + " supercollection members parsed.");
+	this.model = this;
+    }
+
+    public void destroy() {
+	if(controls != null) {
+	    controls.destroy();
+	    controls = null;
+	}
+	if(collection_checklist_model != null) {
+	    collection_checklist_model.clear();
+	    collection_checklist_model = null;
+	}
+    }
+
+    private void addSuperCollection(SuperCollection supercollection) {
+	if(!contains(supercollection)) {
+	    add(getSize(), supercollection);
+	}
+    }
+
+    /** Method to retrieve the control for this manager.
+     * @return the Control for editing supercollection settings
+     */
+    public Control getControls() {
+	if(controls == null) {
+	    // Build controls
+	    this.controls = new SuperCollectionControl();
+	}
+	return controls;
+    }
+
+    public SuperCollection getSuperCollection(String collection_name) {
+	SuperCollection result = null;
+	int size = getSize();
+	for(int i = 0; result == null && i < size; i++) {
+	    SuperCollection supercollection = (SuperCollection) getElementAt(i);
+	    if(supercollection.getName().equals(collection_name)) {
+		result = supercollection;
+	    }
+	    supercollection = null;
+	}
+	return result;
+    }
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+
+    }
+
+    private void removeSuperCollection(SuperCollection supercollection) {
+	if(contains(supercollection)) {
+	    remove(supercollection);
+	}
+    }
+
+    /** Provides controls for altering the SuperCollection settings. */
+    private class SuperCollectionControl
+	extends JPanel
+	implements Control {
+
+	private boolean init = true;
+	private CheckList collection_checklist = null;
+
+	SuperCollectionControl() {
+	    super();
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    // Creation
+	    JPanel header_panel = new DesignPaneHeader("CDM.GUI.SuperCollection", "xcollectionsearching");
+
+	    collection_checklist = new CheckList(false);
+	    buildModel();
+	    collection_checklist.setListData(collection_checklist_model);
+
+	    JPanel collection_checklist_pane = new JPanel();
+            collection_checklist_pane.setComponentOrientation(Dictionary.getOrientation());
+	    collection_checklist_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    collection_checklist_pane.setLayout(new BorderLayout());
+	    collection_checklist_pane.add(new JScrollPane(collection_checklist), BorderLayout.CENTER);
+
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(header_panel, BorderLayout.NORTH);
+	    add(collection_checklist_pane, BorderLayout.CENTER);
+	}
+
+	public void destroy() {
+	}
+
+	public void gainFocus() {
+	}
+
+	public void loseFocus() {
+	    int super_collections_count = 0;
+	    // Retrieve the current supercollections
+	    ArrayList supercollections = children();
+	    // Now iterate through the checklist, and for each checked box found ensure the Supercollection exists, and ensure its assigned. Remove any supercollections altered in this way from the temporary array list
+	    // we ignore the current coll at the moment
+	    int size = collection_checklist_model.size();
+	    for(int i = 0; i < size; i++) {
+		CheckListEntry entry = (CheckListEntry) collection_checklist_model.get(i);
+		if(entry.isSelected()) {
+		    String collection_name = (String) entry.getProperty();
+		    if (!collection_name.equals(current_coll_name)) {
+			SuperCollection supercollection = getSuperCollection(collection_name);
+			// Create the supercollection element if necessary
+			if(supercollection == null) {
+			    Element supercollection_element = root.getOwnerDocument().createElement(StaticStrings.COLLECTION_ELEMENT);
+			    supercollection = new SuperCollection(supercollection_element);
+			    supercollection.setName(collection_name);
+			    addSuperCollection(supercollection);
+			}
+			else {
+			    supercollections.remove(supercollection);
+			}
+			supercollection.setAssigned(true);
+			super_collections_count++;
+		    }
+		}
+	    }
+	    if (super_collections_count > 0) {
+		// we have some super colls, add in the current collection
+		SuperCollection supercollection = getSuperCollection(current_coll_name);
+		// Create the supercollection element if necessary
+		if(supercollection == null) {
+		    Element supercollection_element = root.getOwnerDocument().createElement(StaticStrings.COLLECTION_ELEMENT);
+		    supercollection = new SuperCollection(supercollection_element);
+		    supercollection.setName(current_coll_name);
+		    addSuperCollection(supercollection);
+		}
+		else {
+		    supercollections.remove(supercollection);
+		}
+		model.root.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
+	    } else {
+		// current collection is the only one - don't bother adding it, because assigned is false, and we don't want to update the config file for an unassigned item.
+		model.root.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
+	    }
+	    
+	    // Any collections left in the temporary list have been unselected, so delete them
+	    for(int j = supercollections.size(); j > 0; j--) {
+		SuperCollection supercollection = (SuperCollection) supercollections.get(j - 1);
+		removeSuperCollection(supercollection);
+	    }
+	}
+
+	
+      private void buildModel()
+      {
+	collection_checklist_model = new ArrayList();
+	current_coll_name = CollectionManager.getLoadedCollectionName(true);
+	File collect_directory = new File(Gatherer.getCollectDirectoryPath());
+	add_collections_to_model(collect_directory);
+      }
+
+      private void add_collections_to_model(File collect_directory) {
+	File[] possible_collections = collect_directory.listFiles();
+	for(int i = 0; possible_collections != null && i < possible_collections.length; i++) {
+	  String file_name = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+	  File collect_cfg_file = new File(possible_collections[i], file_name);
+	  if (collect_cfg_file.exists()) {
+	    BasicCollectionConfiguration collect_cfg = new BasicCollectionConfiguration(collect_cfg_file);
+	    // look to see if group set?
+	    // => if it is, don't add, but recurse to look for child collections in collect-group
+	    if (collect_cfg.getCollectGroup().equals("true")) {
+	      add_collections_to_model(possible_collections[i]);
+	    } else {
+	      // its a collection
+	      StringBuffer title_buffer = new StringBuffer(collect_cfg.getName());
+	      title_buffer.append(StaticStrings.SPACE_CHARACTER);
+	      title_buffer.append(StaticStrings.OPEN_PARENTHESIS_CHARACTER);
+	      title_buffer.append(collect_cfg.getShortName());
+	      title_buffer.append(StaticStrings.CLOSE_PARENTHESIS_CHARACTER);
+	      String collection_title = title_buffer.toString();
+	      title_buffer = null;
+	      String collection_name = collect_cfg.getShortName();
+	      // We have to block the model collection.
+	      if (collect_cfg.getName().equals("**title**")) {
+		continue;
+	      }
+
+	      // The current collection is always selected.
+	      CheckListEntry entry = new CheckListEntry(collection_title);
+	      entry.setProperty(collection_name);
+	      entry.setSelected(getSuperCollection(collection_name) != null || collection_name.equals(current_coll_name));
+	      entry.setFixed(collection_name.equals(current_coll_name));
+	      collection_checklist_model.add(entry);
+	    }
+	  }
+	  
+	}
+      }
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/TranslationView.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/TranslationView.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/cdm/TranslationView.java	(revision 31635)
@@ -0,0 +1,755 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.cdm;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.table.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.DesignPaneHeader;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+/** This class provides a graphical interface to allow a user to quickly and conviently (ie all in one place) translate the text fragments associated with general metadata and indexes into each of the assigned languages in the collection. It should provide clear controls for the editing of these text fragments, plus clear indicate what languages still need further translation, which it will do through a combination of coloring and other visual indicators.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class TranslationView {
+
+    private Control controls;
+
+    static final private Dimension COMPONENT_SIZE = new Dimension(225,25);
+    static final private int LANGUAGE_WIDTH = 75;
+    static final private String GENERAL_STR = "General:";
+    static final private String INDEX_STR = "Index:";
+    static final private String LEVEL_STR = "Level:";
+    static final private String PARTITION_STR = "Partitions:";
+
+    public TranslationView() {
+	DebugStream.println("TranslationView: Initialized.");
+    }
+
+    public void destroy() {
+	if(controls != null) {
+	    controls.destroy();
+	    controls = null;
+	}
+    }
+
+    public Control getControls() {
+	if(controls == null) {
+	    controls = new TranslationControl();
+	}
+	return controls;
+    }
+
+    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+
+    }
+
+    private Object[] getFeaturesList() {
+	ArrayList features_model = new ArrayList();
+
+	// Add the indexes
+	ArrayList indexes = CollectionDesignManager.index_manager.getIndexes();
+	int indexes_size = indexes.size();
+	for(int j = 0; j < indexes_size; j++) {
+	    Object bob = new BobTheMagicalComparableWrapper(indexes.get(j));
+	    if(!features_model.contains(bob)) {
+		features_model.add(bob);
+	    }
+	}
+	// Add the levels, if any
+	ArrayList levels = CollectionDesignManager.index_manager.getLevels();
+	if (levels !=null) {
+	  int levels_size = levels.size();
+	  for (int j=0; j<levels.size(); j++) {
+	    Object bob = new BobTheMagicalComparableWrapper(levels.get(j));
+	    if(!features_model.contains(bob)) {
+	      features_model.add(bob);
+	    }
+	  }
+	}
+	// We also add subindexes.
+	ArrayList subindexes = CollectionDesignManager.subcollectionindex_manager.getSubcollectionIndexes();
+	int subindexes_size = subindexes.size();
+	for(int j = 0; j < subindexes_size; j++) {
+	    Object bob = new BobTheMagicalComparableWrapper(subindexes.get(j));
+	    if(!features_model.contains(bob)) {
+		features_model.add(bob);
+	    }
+	}
+
+	// Finally we add any remaining metadata as general
+	ArrayList general_metadata = CollectionDesignManager.collectionmeta_manager.getMetadata();
+	for(int i = 0; i < general_metadata.size(); i++) {
+	    CollectionMeta metadata = (CollectionMeta) general_metadata.get(i);
+	    if(!metadata.isSpecial()) {
+		Object bob = new BobTheMagicalComparableWrapper(metadata.getName());
+		if(!features_model.contains(bob)) {
+		    features_model.add(bob);
+		}
+	    }
+	}
+
+	Collections.sort(features_model);
+
+	return features_model.toArray();
+    }
+
+    private class BobTheMagicalComparableWrapper
+	implements Comparable {
+	private Object content;
+	private String text;
+	BobTheMagicalComparableWrapper(Object content) {
+	    this.content = content;
+	}
+	public int compareTo(Object object) {
+	    if(object == null) {
+		object = "";
+	    }
+	    if(text == null) {
+		toString();
+	    }
+	    return text.compareTo(object.toString());
+	}
+
+	/** Equals is used by contains and since we want to prevent double up of metadata we compare differently than in compareTo. */
+	public boolean equals(Object object) {
+	    return (object != null && content.toString().equals(((BobTheMagicalComparableWrapper)object).getContent().toString()));
+	}
+
+	public Object getContent() {
+	    return content;
+	}
+
+	public String getName() {
+	    if(content instanceof Index) {
+		return StaticStrings.STOP_CHARACTER + ((Index)content).getID();
+	    }
+	    // level
+	    else if (content instanceof IndexOption) {
+	      return StaticStrings.STOP_CHARACTER + ((IndexOption)content).getName();
+	    }
+	    else if (content instanceof SubcollectionIndex) {
+	    	return StaticStrings.STOP_CHARACTER + ((SubcollectionIndex)content).getID();
+	    }
+	    else {
+		return content.toString();
+	    }
+	}
+
+	public String toString() {
+	    if(text == null) {
+		String temp = content.toString();
+		if(temp.indexOf(StaticStrings.SPACE_CHARACTER) != -1) {
+		    temp = temp.substring(0, temp.indexOf(StaticStrings.SPACE_CHARACTER));
+		}
+		if(content instanceof Index) {
+		    text = INDEX_STR + temp;
+		}
+		else if (content instanceof IndexOption) {
+		  text = LEVEL_STR + temp;
+		}
+		else if(content instanceof SubcollectionIndex) {
+		    text = PARTITION_STR + temp;
+		}
+		else {
+		    text = GENERAL_STR + temp;
+		}
+	    }
+	    return text;
+	}
+    }
+
+    private class TranslationControl
+	extends JPanel 
+	implements Control {
+
+	private boolean ignore_event = false;
+	private FragmentTableModel fragment_table_model;
+	private JButton add_button;
+	private JButton remove_button;
+	private JButton replace_button;
+	private JComboBox language_combobox;
+	private JList features_list;
+	private JTable fragment_table;
+	private JTextArea default_area;
+	private JTextArea translation_area;
+
+	TranslationControl() {
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    fragment_table_model = new FragmentTableModel("", new TreeSet(), new ArrayList());
+	    // Creation
+	    JPanel header_panel = new DesignPaneHeader("CDM.GUI.Translation", "translatetext");
+
+	    JPanel center_panel = new JPanel();
+            center_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel selection_panel = new JPanel();
+            selection_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel component_selection_panel = new JPanel();
+            component_selection_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JLabel component_label = new JLabel(Dictionary.get("CDM.TranslationManager.Affected_Features"));
+            component_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    Object[] features = getFeaturesList();
+	    features_list = new JList(features);
+	    features_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+            if (features != null && features.length >0){
+		features_list.setSelectedIndex(0);
+	    }
+
+	    JPanel fragment_selection_panel = new JPanel();
+	    fragment_selection_panel.setComponentOrientation(Dictionary.getOrientation());
+            JLabel fragment_label = new JLabel(Dictionary.get("CDM.TranslationManager.Assigned_Fragments"));
+            fragment_label.setComponentOrientation(Dictionary.getOrientation());
+	    
+	    fragment_table = new JTable(fragment_table_model);
+            fragment_table.setComponentOrientation(Dictionary.getOrientation());
+	    fragment_table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
+	    fragment_table.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    fragment_table.setColumnSelectionAllowed(false);
+	    fragment_table.setDefaultRenderer(Object.class, new FragmentTableRenderer());
+	    fragment_table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+	    JScrollPane table_scroll = new JScrollPane(fragment_table);
+	    table_scroll.getViewport().setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    table_scroll.setOpaque(true);
+
+	    JPanel south_panel = new JPanel();
+            south_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel text_panel = new JPanel();
+            text_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel language_panel = new JPanel();
+            language_panel.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JLabel language_label = new JLabel(Dictionary.get("CDM.TranslationManager.Language"));
+	    language_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    language_combobox = new JComboBox(CollectionDesignManager.language_manager.getLanguageCodes().toArray());
+	    language_combobox.setOpaque(!Utility.isMac());
+	    language_combobox.setPreferredSize(COMPONENT_SIZE);
+	    language_combobox.setRenderer(new LanguageListCellRenderer());
+	    language_combobox.setToolTipText(Dictionary.get("CDM.TranslationManager.Language_Tooltip"));
+	    language_combobox.setComponentOrientation(Dictionary.getOrientation());
+            
+	    JPanel default_text_panel = new JPanel();
+            default_text_panel.setComponentOrientation(Dictionary.getOrientation());
+	    JLabel default_label = new JLabel(Dictionary.get("CDM.TranslationManager.Default_Text"));
+	    default_label.setComponentOrientation(Dictionary.getOrientation());
+	    default_area = new JTextArea();
+            default_area.setComponentOrientation(Dictionary.getOrientation());
+	    default_area.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    default_area.setEditable(false);
+	    default_area.setLineWrap(true);
+	    default_area.setWrapStyleWord(true);
+
+	    JPanel translated_text_panel = new JPanel();
+            translated_text_panel.setComponentOrientation(Dictionary.getOrientation());
+	    JLabel translation_label = new JLabel(Dictionary.get("CDM.TranslationManager.Translation"));
+            translation_label.setComponentOrientation(Dictionary.getOrientation());
+	    translation_area = new JTextArea();
+            translation_area.setComponentOrientation(Dictionary.getOrientation());
+	    translation_area.setBackground(Configuration.getColor("coloring.disabled", false));
+	    translation_area.setEnabled(false);
+	    translation_area.setLineWrap(true);
+	    translation_area.setWrapStyleWord(true);
+	    translation_area.setToolTipText(Dictionary.get("CDM.TranslationManager.Translation_Tooltip"));
+
+	    JPanel button_pane = new JPanel();
+            button_pane.setComponentOrientation(Dictionary.getOrientation());
+	    add_button = new GLIButton(Dictionary.get("CDM.TranslationManager.Add"), Dictionary.get("CDM.TranslationManager.Add_Tooltip"));
+	    add_button.setEnabled(false);
+
+	    replace_button = new GLIButton(Dictionary.get("CDM.TranslationManager.Replace"), Dictionary.get("CDM.TranslationManager.Replace_Tooltip"));
+	    replace_button.setEnabled(false);
+
+	    remove_button = new GLIButton(Dictionary.get("CDM.TranslationManager.Remove"), Dictionary.get("CDM.TranslationManager.Remove_Tooltip"));
+	    remove_button.setEnabled(false);
+
+
+	    // Connection
+	    add_button.addActionListener(new AddListener());
+	    remove_button.addActionListener(new RemoveListener());
+	    replace_button.addActionListener(new ReplaceListener());
+	    language_combobox.addActionListener(new LanguageActionListener());
+	    translation_area.getDocument().addDocumentListener(new TranslationDocumentListener());
+	    features_list.addListSelectionListener(new FeaturesListSelectionListener());
+	    fragment_table.getSelectionModel().addListSelectionListener(new FragmentListSelectionListener());
+
+	    // Layout
+	    component_selection_panel.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    component_selection_panel.setLayout(new BorderLayout());
+	    component_selection_panel.add(component_label, BorderLayout.NORTH);
+	    component_selection_panel.add(new JScrollPane(features_list), BorderLayout.CENTER);
+
+	    fragment_selection_panel.setLayout(new BorderLayout());
+	    fragment_selection_panel.add(fragment_label, BorderLayout.NORTH);
+	    fragment_selection_panel.add(table_scroll, BorderLayout.CENTER);
+
+	    selection_panel.setLayout(new GridLayout(2,1,0,5));
+	    selection_panel.add(component_selection_panel);
+	    selection_panel.add(fragment_selection_panel);
+
+	    default_text_panel.setLayout(new BorderLayout());
+	    default_text_panel.add(default_label, BorderLayout.NORTH);
+	    default_text_panel.add(new JScrollPane(default_area), BorderLayout.CENTER);
+
+	    translated_text_panel.setLayout(new BorderLayout());
+	    translated_text_panel.add(translation_label, BorderLayout.NORTH);
+	    translated_text_panel.add(new JScrollPane(translation_area), BorderLayout.CENTER);
+
+	    text_panel.setLayout(new GridLayout(1,2,5,0));
+	    text_panel.add(default_text_panel);
+	    text_panel.add(translated_text_panel);
+
+	    language_panel.setLayout(new BorderLayout(5,0));
+	    language_panel.add(language_label, BorderLayout.LINE_START);
+	    language_panel.add(language_combobox, BorderLayout.CENTER);
+
+	    south_panel.setLayout(new BorderLayout());
+	    south_panel.add(language_panel, BorderLayout.NORTH);
+	    south_panel.add(text_panel, BorderLayout.CENTER);
+
+	    center_panel.setLayout(new GridLayout(2,1,0,5));
+	    center_panel.add(selection_panel);
+	    center_panel.add(south_panel);
+
+	    button_pane.setBorder(BorderFactory.createEmptyBorder(2,0,0,0));
+	    button_pane.setLayout(new GridLayout(1,3));
+	    button_pane.add(add_button);
+	    button_pane.add(replace_button);
+	    button_pane.add(remove_button);
+
+	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	    setLayout(new BorderLayout());
+	    add(header_panel, BorderLayout.NORTH);
+	    add(center_panel, BorderLayout.CENTER);
+	    add(button_pane, BorderLayout.SOUTH);
+	}
+
+	public void destroy() {
+	}
+
+	public void gainFocus() {
+	    // Rebuild the features model, just incase features have been added or removed.
+	    if(features_list != null) {
+		Object selected_feature = features_list.getSelectedValue();
+		features_list.setListData(getFeaturesList());
+		if(selected_feature != null) {
+		    features_list.setSelectedValue(selected_feature, true);
+		}
+	    }
+	}
+
+	public void loseFocus() {
+	}
+
+
+	private class AddListener
+	    implements ActionListener {
+	    
+	    public void actionPerformed(ActionEvent event) {
+		ignore_event = true;
+		String translation_text = translation_area.getText();
+		if(translation_text.length() > 0) {
+		    // If there is no current fragment table selection, but translation text is not an empty string, then this is a new fragment. Create new collection metadata, refresh the fragment table, then ensure that the new metadata is selected. Remember that it is the selected metadata to which changes will be applied.
+		    BobTheMagicalComparableWrapper selected_feature = (BobTheMagicalComparableWrapper)features_list.getSelectedValue();
+		    String language = (String) language_combobox.getSelectedItem();
+		    CollectionMeta metadatum = new CollectionMeta(selected_feature.getName(), language);
+		    metadatum.setValue(translation_text);
+		    CollectionDesignManager.collectionmeta_manager.addMetadatum(metadatum);
+		    fragment_table.clearSelection();
+		    ArrayList metadata = CollectionDesignManager.collectionmeta_manager.getMetadata(selected_feature.getName());
+		    fragment_table_model.setData(selected_feature.getName(), CollectionDesignManager.collectionmeta_manager.getLanguages(), metadata);
+		    int index = fragment_table_model.getMetadataIndexByLanguage(language);
+		    ///ystem.err.println("I want to select the row " + index + " out of " + metadata.size());
+		    fragment_table.setRowSelectionInterval(index, index);
+		    metadatum = null;
+		    language = null;
+		    selected_feature = null;
+		    translation_text = null;
+		    remove_button.setEnabled(true);
+		}
+		add_button.setEnabled(false);
+		replace_button.setEnabled(false);
+		ignore_event = false;
+	    }
+	}
+
+	private class RemoveListener
+	    implements ActionListener {
+	    
+	    public void actionPerformed(ActionEvent event) {
+		ignore_event = true;
+		int index = -1;
+		if((index = fragment_table.getSelectedRow()) != -1) {
+		    CollectionMeta metadata = fragment_table_model.getMetadata(index);
+		    CollectionDesignManager.collectionmeta_manager.removeMetadata(metadata);
+		    fragment_table_model.remove(index);
+		    // If a remove occured then enable add
+		    add_button.setEnabled(true);
+		}
+		// Either way disable replace and remove
+		replace_button.setEnabled(false);
+		remove_button.setEnabled(false);
+		ignore_event = false;
+	    }
+	}
+
+	private class ReplaceListener 
+	    implements ActionListener {
+
+	    public void actionPerformed(ActionEvent event) {
+		ignore_event = true;
+		int index = -1;
+		if((index = fragment_table.getSelectedRow()) != -1) {
+		    String translation_text = translation_area.getText();
+		    // Get the selected collection metadata
+		    CollectionMeta metadata = fragment_table_model.getMetadata(index);
+		    // Update the fragment metadata and ask the table to repaint the appropriate row
+		    metadata.setValue(translation_text);
+		    fragment_table_model.fireTableRowsUpdated(index, index);
+		    metadata = null;
+		    remove_button.setEnabled(true);
+		}
+		// Either way disable replace
+		add_button.setEnabled(false);
+		replace_button.setEnabled(false);
+		ignore_event = false;
+	    }
+	}
+
+	private class FeaturesListSelectionListener
+	    implements ListSelectionListener {
+	    public void valueChanged(ListSelectionEvent event) {
+		if(!event.getValueIsAdjusting() && !ignore_event) {
+		    ///ystem.err.println("FeaturesListSelectionListener");
+		    if(!features_list.isSelectionEmpty()) {
+			// Determine the features name. Remember to remove our helpful source prefix (delimited by ':').
+			BobTheMagicalComparableWrapper selected_feature = (BobTheMagicalComparableWrapper)features_list.getSelectedValue();
+			// Retrieve the default language translation, or otherwise the first match, to be default text.
+			CollectionMeta default_metadata = CollectionDesignManager.collectionmeta_manager.getMetadatum(selected_feature.getName());
+			if(default_metadata != null) {
+			    default_area.setText(default_metadata.getValue(CollectionMeta.TEXT));
+			}
+			// Update the text fragment table.
+			fragment_table.clearSelection();
+			fragment_table_model.setData(selected_feature.getName(), CollectionDesignManager.collectionmeta_manager.getLanguages(), CollectionDesignManager.collectionmeta_manager.getMetadata(selected_feature.getName()));
+			selected_feature = null;
+			// Now we check whatever value is currently selected in the combobox to see if it is valid to add.
+			String language_name = (String) language_combobox.getSelectedItem();
+			int index = fragment_table_model.getMetadataIndexByLanguage(language_name);
+			if(index != -1) {
+			    CollectionMeta metadata = fragment_table_model.getMetadata(index);
+			    fragment_table.setRowSelectionInterval(index, index);
+			    if(metadata != null) {
+				translation_area.setText(metadata.getValue(CollectionMeta.TEXT));
+			    }
+			    else {
+				translation_area.setText("");
+			    }
+			    metadata = null;
+			}
+			else {
+			    translation_area.setText("");
+			}
+			// Update and enable the text area
+			translation_area.setEnabled(true);
+			translation_area.setBackground(Configuration.getColor("coloring.editable_background", false));
+		    }
+		    else {
+			default_area.setText("");
+			fragment_table.clearSelection();
+			fragment_table_model.setData("", new TreeSet(), new ArrayList());
+			translation_area.setText("");
+			// Update and disable the text area
+			translation_area.setText("");
+			translation_area.setEnabled(false);
+			translation_area.setBackground(Configuration.getColor("coloring.disabled", false));
+		    }
+		}
+	    }
+	}
+
+	private class FragmentListSelectionListener
+	    implements ListSelectionListener {
+	    public void valueChanged(ListSelectionEvent event) {
+		if(!event.getValueIsAdjusting() && !ignore_event) {
+		    ignore_event = true;
+		    ///ystem.err.println("FragmentListSelectionListener");
+		    int index = -1;
+		    if((index = fragment_table.getSelectedRow()) != -1) {
+			// A row has been selected. Retrieve the collection metadata.
+			CollectionMeta metadatum = fragment_table_model.getMetadata(index);
+			if(metadatum != null) {
+			    // Update the combobox to show the current language.
+			    String language = metadatum.getLanguage();
+			    ///ystem.err.println("Searching for the language: " + language_name);
+			    for(int i = 0; i < language_combobox.getItemCount(); i++) {
+				if(language_combobox.getItemAt(i).toString().equals(language)) {
+				    language_combobox.setSelectedIndex(i);
+				}
+			    }
+			    translation_area.setText(metadatum.getValue(CollectionMeta.TEXT));
+			    remove_button.setEnabled(!metadatum.isDummy());
+			}
+			else {
+			    remove_button.setEnabled(false);
+			}
+		    }
+		    else {
+			translation_area.setText("");
+			remove_button.setEnabled(false);
+		    }
+		    ignore_event = false;
+		}
+	    }
+	}
+
+	private class FragmentTableModel
+	    extends AbstractTableModel {
+
+	    private ArrayList metadata;
+	    private String feature_name;
+	    private TreeSet languages;
+
+	    FragmentTableModel(String feature_name, TreeSet languages, ArrayList metadata) {
+		this.feature_name = feature_name;
+		this.languages = languages;
+		this.metadata = metadata;
+	    }
+	    public int getRowCount() {
+	       // The row count is equal to the maximum number of languages currently assigned in the collection.
+		return languages.size();
+	    }
+	    public int getColumnCount() {
+		return 2;
+	    }
+
+	    public String getColumnName(int column) {
+		if (column == 0) {
+		    return Dictionary.get("CDM.TranslationManager.Language_Column");
+		}
+		if (column == 1) {
+		    return Dictionary.get("CDM.TranslationManager.Fragment_Column");
+		}
+		return "";
+	    }
+
+	    /** Attempt to retrieve the metadata associated with a certain row - however not all rows actually have metadata with them (say the French row where no metadata has been set).
+	     * @param row the row number as an int
+	     * @return the CollectionMeta requested, which may be a dummy metadata pair
+	     */
+	    public CollectionMeta getMetadata(int row) {
+		// Determine what language we are talking about
+		String language = null;
+		Iterator language_iterator = languages.iterator();
+		int current_row = 0;
+		while(current_row != row && language_iterator.hasNext()) {
+		    language_iterator.next();
+		    current_row++;
+		}
+		if(current_row == row) {
+		    language = (String) language_iterator.next();
+		    return getMetadata(language);
+		}
+		language_iterator = null;
+		return null;
+	    }
+
+	    public CollectionMeta getMetadata(String language) {
+		// Attempt to retrieve metadata with that language
+		for(int i = 0; i < metadata.size(); i++) {
+		    CollectionMeta metadatum = (CollectionMeta) metadata.get(i);
+		    if(metadatum.getLanguage().equals(language)) {
+			return metadatum;
+		    }
+		}
+		return new CollectionMeta(feature_name, language, true);
+	    }
+
+	    public int getMetadataIndexByLanguage(String language) {
+		///ystem.err.println("Find the index for the language " + language + " (out of " + languages.size() + ")");
+		// First we have to iterate to the correct place
+		Iterator language_iterator = languages.iterator();
+		int current_row = 0;
+		while(language_iterator.hasNext()) {
+		    String target = (String)language_iterator.next();
+		    if(language.equals(target)) {
+			///ystem.err.println("Matches " + target);
+			return current_row;
+		    }
+		    else {
+			///ystem.err.println("Doesn't match " + target);
+			current_row++;
+		    }
+		}
+		///ystem.err.println("No such language in model: -1");
+		return -1;
+	    }
+
+	    public Object getValueAt(int row, int column) {
+		if(0 <= row && row < languages.size()) {
+		    // First we have to iterate to the correct place
+		    Iterator language_iterator = languages.iterator();
+		    int current_row = 0;
+		    while(current_row != row && language_iterator.hasNext()) {
+			language_iterator.next();
+			current_row++;
+		    }
+		    if(current_row == row) {
+			String language = (String)language_iterator.next();
+			if(column == 0) {
+			    return CollectionDesignManager.language_manager.getLanguageName(language);
+			}
+			else {
+			    CollectionMeta metadatum = getMetadata(language);
+			    return metadatum.getValue(CollectionMeta.TEXT);
+			}
+		    }
+		    language_iterator = null;
+		}
+		return "#Error";
+	    }
+
+	    public void remove(int index) {
+		metadata.remove(index);
+		fireTableRowsDeleted(index, index);
+	    }
+
+	    public void setData(String feature_name, TreeSet languages, ArrayList metadata) {
+		this.feature_name = feature_name;
+		this.languages = languages;
+		this.metadata = metadata;
+		fireTableDataChanged();
+	    }
+
+	    public int size() {
+		return metadata.size();
+	    }
+	}
+
+	private class FragmentTableRenderer
+	    extends DefaultTableCellRenderer {
+	    /** Retrieve the component used to rubberstamp the table based on the given parameters.
+	     * @param table The <strong>JTable</strong> to rendered.
+	     * @param value The <strong>Object</strong> whose representation will be shown in the table row.
+	     * @param isSelected <i>true</i> iff the column/row is selected.
+	     * @param hasFocus <i>true</i> iff the column/row is in focus.
+	     * @param row An <i>int</i> specifying the target row.
+	     * @param column An <i>int</i> specifying the target column.
+	     * @see org.greenstone.gatherer.cdm.Language
+	     */
+	    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
+		JComponent component = (JComponent) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+		if(isSelected) {
+		    component.setBackground(Configuration.getColor("coloring.workspace_heading_background", false));
+		}
+		else {
+		    component.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+		}
+		return component;
+	    }
+	}
+
+	private class LanguageActionListener
+	    implements ActionListener {
+	    public void actionPerformed(ActionEvent event) {
+		if(!ignore_event && features_list.getSelectedIndex()>-1) {
+		    ignore_event = true;
+		    ///ystem.err.println("LanguageActionListener");
+		    // If the newly selected language value already exists in the fragment table, and that row isn't the one selected, then select that row the text area.
+		    String language_name = language_combobox.getSelectedItem().toString();
+		    int index = fragment_table_model.getMetadataIndexByLanguage(language_name);
+		    if(index != -1) {
+			CollectionMeta metadata = fragment_table_model.getMetadata(index);
+			fragment_table.setRowSelectionInterval(index, index);
+			translation_area.setText(metadata.getValue(CollectionMeta.TEXT));
+		    }
+		    else {
+			fragment_table.clearSelection();
+			translation_area.setText("");
+		    }
+		    // Ready the text area
+		    translation_area.setEnabled(true);
+		    translation_area.setBackground(Configuration.getColor("coloring.editable_background", false));
+		    ignore_event = false;
+		}
+	    }
+	}
+
+	private class TranslationDocumentListener
+	    implements DocumentListener {
+	    /** Gives notification that an attribute or set of attributes changed. */
+	    public void changedUpdate(DocumentEvent e) {
+		update();
+	    }
+	    /** Gives notification that there was an insert into the document. */
+	    public void insertUpdate(DocumentEvent e) {
+		update();
+	    }
+	    /** Gives notification that a portion of the document has been removed. */
+	    public void removeUpdate(DocumentEvent e) {
+		update();
+	    }
+	    /** The text area has changed in some way. Given that this can only happed when we are editing or adding a text fragment we better respond appropriately. */
+	    private void update() {
+		String translation_text = translation_area.getText();
+		// Determine if add should be enabled. You can only add if the current text fragment doesn't already exist in the fragment table for the given language
+		String language = (String) language_combobox.getSelectedItem();
+		CollectionMeta metadatum = fragment_table_model.getMetadata(language);
+		add_button.setEnabled(translation_text.length() > 0 && (metadatum.isDummy() || fragment_table_model.getMetadataIndexByLanguage(language) == -1));
+		language = null;
+		// Determine if replace should be enabled. Replace is only enabled it the text fragment is different from the one in the current fragment table selection.
+		if(fragment_table.getSelectedRowCount() > 0) {
+		    replace_button.setEnabled(!metadatum.isDummy() && !translation_text.equals(fragment_table.getValueAt(fragment_table.getSelectedRow(), 1)));
+		}
+		// Nothing selected, nothing to replace
+		else {
+		    replace_button.setEnabled(false);
+		}
+		translation_text = null;
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/BasicCollectionConfiguration.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/BasicCollectionConfiguration.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/BasicCollectionConfiguration.java	(revision 31635)
@@ -0,0 +1,361 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.collection;
+
+import java.io.*;
+import java.util.ArrayList;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.CommandTokenizer;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+/** This class provides access to a collection configuration file. This version accepts either a valid xml document or a historical collect.cfg file. */
+public class BasicCollectionConfiguration
+    implements Comparable {
+    
+    private File file;
+    private String creator = "";
+    private String description = "";
+    private String maintainer = "";
+    private String name = "";
+    private String is_public = "";
+    private String collectgroup = "false";
+    
+  private String short_name = "";
+  
+    private String site = null; // used for gs3 colls
+    
+    public BasicCollectionConfiguration(File file) {
+	this.file = file;
+	try {
+	    String filename = file.getName().toLowerCase();
+	    if(!file.exists()) {
+		return;
+	    }
+	    if(filename.endsWith(".cfg")) {
+		FileInputStream fis = new FileInputStream(file);
+		InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
+		BufferedReader br = new BufferedReader(isr);
+		String command = null;
+		while((command = br.readLine()) != null) {
+		  if (command.startsWith("#")) {
+		    // a comment
+		    continue;
+		  }
+		    if(command.length() > 0) {
+			// We have to test the end of command for the special character '\'. If found, remove it and append the next line, then repeat.
+			while(command.trim().endsWith("\\")) {
+			    command = command.substring(0, command.lastIndexOf("\\"));
+			    String next_line = br.readLine();
+			    if (next_line != null) {
+				command = command + "\n" + next_line;
+			    }
+			    next_line = null;
+			}
+			CommandTokenizer tokenizer = new CommandTokenizer(command, br);
+			String command_type_str = tokenizer.nextToken();
+			if (command_type_str == null) {
+			    // Bad command. Do nothing
+			    continue;
+			}
+			
+			command_type_str = command_type_str.toLowerCase();
+			if(command_type_str.equals(StaticStrings.COLLECTIONMETADATA_CREATOR_STR)) {
+			    creator = tokenizer.nextToken();
+			}
+			else if(command_type_str.equals(StaticStrings.COLLECTIONMETADATA_MAINTAINER_STR)) {
+			    maintainer = tokenizer.nextToken();
+			}
+			else if(command_type_str.equals(StaticStrings.COLLECTIONMETADATA_COLLECTGROUP_STR)) {
+			    collectgroup = tokenizer.nextToken();
+			}
+			else if(command_type_str.equalsIgnoreCase(StaticStrings.COLLECTIONMETADATA_STR)) {
+			    String meta_type_str = tokenizer.nextToken();
+			    String value_str = tokenizer.nextToken();
+			    // check for language
+			    String language_str = StaticStrings.ENGLISH_LANGUAGE_STR; // assume the no-lang ones are english, but we shouldn't really do this.
+			    if(meta_type_str != null && value_str != null) {
+				meta_type_str = meta_type_str.toLowerCase();
+				if(value_str.startsWith(StaticStrings.LBRACKET_CHARACTER) && value_str.endsWith(StaticStrings.RBRACKET_CHARACTER)) {
+				    language_str = value_str.substring(value_str.indexOf(StaticStrings.EQUALS_CHARACTER) + 1, value_str.length() - 1);
+				    language_str = language_str.toLowerCase();
+				    value_str = tokenizer.nextToken();
+				}
+				// now we can work out which coll meta we are dealing with
+				if (meta_type_str.equals(StaticStrings.COLLECTIONMETADATA_COLLECTIONNAME_STR)) {
+				    // If this is the first collection title found, then use it, otherwise search for one that more closely matches our choosen interface language
+				    if (name == null || language_str.equals(Dictionary.getLanguage())) {
+					name = Codec.transform(value_str, Codec.GREENSTONE_TO_TEXT);
+				    }
+				}
+				else if (meta_type_str.equals(StaticStrings.COLLECTIONMETADATA_COLLECTIONEXTRA_STR)) {
+				    // Again we are either looking for the first description, then after that a language specific one
+				    if (description == null || language_str.equals(Dictionary.getLanguage())) {
+					description = Codec.transform(value_str, Codec.GREENSTONE_TO_TEXT);
+				    }
+				}
+			    }
+			    language_str = null;
+			    value_str = null;
+			    meta_type_str = null;
+			} else {
+			    // we want to process all the tokens to make sure we get rid of multi line commands before trying to find the next one
+			    while (tokenizer.hasMoreTokens()) {
+			    	tokenizer.nextToken();
+			    }
+
+			} // end of coll meta bit
+			command_type_str = null;
+			tokenizer = null;
+		    } // if command.length > 0
+		} // while
+		command = null;
+		br.close();
+		isr.close();
+		br = null;
+		isr = null;
+	    } // cfg file
+	    ///ystem.err.println("Parsed collect.cfg");
+	    ///ystem.err.println("name		= " + name);
+	    ///ystem.err.println("creator	 = " + creator);
+	    ///ystem.err.println("maintainer  = " + maintainer);
+	    ///ystem.err.println("description = " + description);
+
+	    if(filename.endsWith(".xml")) {
+		
+		Document xml_file_doc = XMLTools.parseXMLFile(file);
+		Element root = xml_file_doc.getDocumentElement();
+
+		Node metadataListNode = XMLTools.getChildByTagName(root, StaticStrings.METADATALIST_STR);
+		Node displayItemListNode = XMLTools.getChildByTagName(root, StaticStrings.DISPLAYITEMLIST_STR);
+
+		// Get the metadataList and displayItemList metadata for the current gli language
+		String default_language = StaticStrings.ENGLISH_LANGUAGE_STR; // is this always the case?
+		String gli_language_code = Configuration.getLanguage(); // definitely returns some language code
+
+		if (metadataListNode != null) {
+		  ArrayList metadata_list = XMLTools.getChildElementsByTagName((Element)metadataListNode, StaticStrings.METADATA_STR);
+
+		  String fallback_lang_creator = "";
+		  String fallback_lang_maintainer = "";
+		  String fallback_lang_is_public = "";
+		  
+		  for (int i=0; i<metadata_list.size(); i++) {
+		    Element meta = (Element) metadata_list.get(i);
+		    String meta_name = meta.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+
+		    // want to get the metadata matching GLI's language, or else fall back on the default language
+		    // version, or else fall back onto the first value for any language provided for the metadata
+		    String meta_lang = meta.getAttribute(StaticStrings.LANG_ATTRIBUTE); // returns "" if no such attribute
+		    if(meta_lang.equals("")) {
+			meta_lang = default_language;
+		    }
+		    
+		    if (meta_name.equals(StaticStrings.COLLECTIONMETADATA_CREATOR_STR)) {
+			if (meta_lang.equals(gli_language_code)) { // metadata's language matched the requested language
+			    creator = XMLTools.getNodeText(meta);
+			} else if (meta_lang.equals(default_language)) { // meta_lang matched fall back language
+			    fallback_lang_creator = XMLTools.getNodeText(meta);
+			} else if (creator.equals("") && fallback_lang_creator.equals("")) { // resort to first value for meta we can find
+			    fallback_lang_creator = XMLTools.getNodeText(meta);
+			}
+		    } else if (meta_name.equals(StaticStrings.COLLECTIONMETADATA_MAINTAINER_STR)) {
+			if (meta_lang.equals(gli_language_code)) {
+			    maintainer = XMLTools.getNodeText(meta);
+			} else if (meta_lang.equals(default_language)) {
+			    fallback_lang_maintainer = XMLTools.getNodeText(meta);
+			} else if (maintainer.equals("") && fallback_lang_maintainer.equals("")) { // resort to first value for meta we can find
+			    fallback_lang_maintainer = XMLTools.getNodeText(meta);
+			}
+		    } else if (meta_name.equals(StaticStrings.COLLECTIONMETADATA_PUBLIC_STR)) {
+			if (meta_lang.equals(gli_language_code)) {
+			    is_public = XMLTools.getNodeText(meta);
+			} else if (meta_lang.equals(default_language)) {
+			    fallback_lang_is_public = XMLTools.getNodeText(meta);
+			} else if (is_public.equals("") && fallback_lang_is_public.equals("")) { // resort to first value for meta we can find
+			    fallback_lang_is_public = XMLTools.getNodeText(meta);
+			}
+		    }		
+		  }
+		  // if no metadata found for the requested language, use the one set in the default language
+		  if(creator.equals("")) {
+		      creator = fallback_lang_creator;
+		  }
+		  if(maintainer.equals("")) {
+		      maintainer = fallback_lang_maintainer;
+		  }
+		  if(is_public.equals("")) {
+		      is_public = fallback_lang_is_public;
+		  }
+		}
+
+
+		if (displayItemListNode != null) {
+		  ArrayList display_list = XMLTools.getChildElementsByTagName((Element)displayItemListNode, StaticStrings.DISPLAYITEM_STR);
+
+		  String fallback_lang_name = "";
+		  String fallback_lang_description = "";
+
+		  for (int i=0; i<display_list.size(); i++) {
+		    Element meta = (Element) display_list.get(i);
+		    String meta_name = meta.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+		    String meta_lang = meta.getAttribute(StaticStrings.LANG_ATTRIBUTE);
+		    if(meta_lang.equals("")) {
+			meta_lang = default_language;
+		    }
+
+		    if (meta_name.equals(StaticStrings.NAME_STR)) {
+			if (meta_lang.equals(gli_language_code)) {
+			    name = XMLTools.getNodeText(meta);
+			} else if (meta_lang.equals(default_language)) {
+			    fallback_lang_name = XMLTools.getNodeText(meta);
+			} else if (name.equals("") && fallback_lang_name.equals("")) {
+			    fallback_lang_name = XMLTools.getNodeText(meta);
+			}
+		    } else if (meta_name.equals(StaticStrings.DESCRIPTION_STR)) {
+			if (meta_lang.equals(gli_language_code)) {
+			    description = XMLTools.getNodeText(meta);
+			} else if (meta_lang.equals(default_language)) {
+			    fallback_lang_description = XMLTools.getNodeText(meta);
+			} else if (description.equals("") && fallback_lang_description.equals("")) {
+			    fallback_lang_description = XMLTools.getNodeText(meta);
+			}
+		    } 
+		    
+		  }
+
+		  if(name.equals("")) {
+		      name = fallback_lang_name;		      
+		  }
+		  if(description.equals("")) {
+		      description = fallback_lang_description;
+		  }
+		}
+	    }
+	    
+	}
+	catch(Exception error) {
+	    DebugStream.println("Error in CollectionConfiguration.<init>(): " + error);
+	    DebugStream.printStackTrace(error);
+	}
+
+    }
+
+    /** Compare this configuration to another for ordering, which essentially compares two strings for ordering.
+     * @param other the other Object which is presumably another basic collection configuration
+     * @return an integer which is either <0, 0 or >0 if this configuration is naturally less than, equal to or greater than the target object
+     */
+    public int compareTo(Object other) {
+	if(other == null) {
+	    return -1;
+	}
+	return toString().compareTo(other.toString());
+    }
+
+    public boolean equals(Object other) {
+	return (compareTo(other) == 0);
+    }
+
+    /** Retrieve the creators email for this collection.
+     * @return a String
+     */
+    public String getCreator() {
+	return creator;
+    }
+
+    public String getDescription() {
+	return description;
+    }
+
+    public File getFile() {
+	return file;
+    }
+    
+    public String getMaintainer() {
+	return maintainer;
+    }
+
+    public String getName() {
+	return name;
+    }
+
+    public String getIsPublic() {
+	return is_public;
+    }
+
+    public String getCollectGroup() {
+	return collectgroup;
+    }
+    
+    /** Retrieve the short name for this collection which, given this current file is in <col_name>/etc/collect.cfg, is the name of this file's parent file's parent.
+     * @return the short name of this collection as a String
+     */
+    public String getShortName() {
+      if (short_name.equals("")) {
+	String collectDir = Gatherer.getCollectDirectoryPath();
+	String currentCollDir = file.getParentFile().getParentFile().getAbsolutePath(); 
+	if (currentCollDir.startsWith(collectDir)) {
+	  short_name = currentCollDir.substring(collectDir.length());
+	  short_name = short_name.replace('\\', '/');
+	} else {
+	  short_name = file.getParentFile().getParentFile().getName();
+	}
+      }
+      return short_name;
+	
+    }
+    
+    /** set the site for this coll */
+    public void setSite(String site) {
+	this.site = site;
+    }
+	
+    
+    /** Display the title for this collection. */
+    public String toString() {
+	if (this.site == null) {
+	    return getName() + StaticStrings.SPACE_CHARACTER + StaticStrings.OPEN_PARENTHESIS_CHARACTER + getShortName() + StaticStrings.CLOSE_PARENTHESIS_CHARACTER;
+	} else {
+	    return getName() + StaticStrings.SPACE_CHARACTER + StaticStrings.OPEN_PARENTHESIS_CHARACTER + getShortName()+", "+this.site + StaticStrings.CLOSE_PARENTHESIS_CHARACTER;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/Collection.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/Collection.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/Collection.java	(revision 31635)
@@ -0,0 +1,526 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.collection;
+
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.cdm.CollectionMeta;
+import org.greenstone.gatherer.cdm.CollectionMetaManager;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+/** Collection provides a common collection point for all the information about a certain collection build session. It keeps a record of several other managers that actually handle the content of the collection, such as metadata sets and metadata itself.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3c
+ */
+public class Collection {
+    /** A reference to the Collection Design Manager. */
+    public CollectionDesignManager cdm;
+    /** A reference to the build ScriptOptions. */
+    public ScriptOptions build_options;
+    /** A reference to the import ScriptOptions. */
+    public ScriptOptions import_options;
+    /** A reference to the schedule ScriptOptions */ 
+    public ScriptOptions schedule_options; 
+    /** true if an error has occurred during construction */
+    public boolean error = false;
+    /** <i>true</i> if the currently loaded collection has been saved since the last significant change, <i>false</i> otherwise. */
+    private boolean saved = false;
+    /*<i>true</i> if the currently loaded collection has had files added since its last build */
+    private boolean filesChanged = false;
+    /*<i>true</i> if the currently loaded collection has had metadata added since its last build */
+    private boolean metadataChanged = false;
+    /** The document around which this collection class is based. */
+    private Document document;
+
+     /** The file the collection is in (the file may not actually exist, such in the case of a legacy collection)! */
+     private File file;
+    /** The name of the argument element. */
+    static final private String ARGUMENT = "Argument";
+    static final private String BASE_COLLECTION = "base_collection";
+    /** The name of the build element. */
+    static final private String BUILD = "Build";
+    /** The name of the build config element. */
+    static final private String BUILD_CONFIG = "BuildConfig";
+    /** The name of the collection xml template. */
+    static final private String COLLECTION_XML_TEMPLATE = "xml/template.col";
+    /** The name of the import element. */
+    static final private String IMPORT = "Import";
+    /** The name of the schedule element. */
+    static final private String SCHEDULE = "Schedule"; 
+    /** The name of the export element. */
+    static final private String EXPORT = "Export";
+    /** The name of the imported attribute. */
+    static final private String IMPORTED = "imported";
+      /** The name of the imported attribute. */
+    static final private String SKIM_FILE = "skimfile"; 
+     
+
+    /** Constructor. */
+    public Collection(File collection_xml) {
+	this.file = collection_xml;
+	// Try to load this collections details.
+       	document = XMLTools.parseXMLFile(collection_xml);
+	// If that fails load the default settings for a collection.
+	if(document == null) {
+	    document = XMLTools.parseXMLFile(COLLECTION_XML_TEMPLATE, true);
+	}
+	if (document == null) {
+	    error = true;
+	    return;
+	}
+	// Point the Configuration class at our gatherer config arguments.
+	Configuration.setCollectionConfiguration(document);
+	if (Gatherer.g_man != null) {
+	    Gatherer.g_man.updateUI();
+	}
+	// Finally create all of the child managers that are directly dependant on a collection
+
+	if (Configuration.fedora_info.isActive()) {
+	    build_options = new ScriptOptions(getValues(BUILD), "g2f-buildcol.pl");
+	    import_options = new ScriptOptions(getValues(IMPORT), "g2f-import.pl");
+	}
+	else {
+	    build_options = new ScriptOptions(getBuildValues(), "buildcol.pl");
+	    import_options = new ScriptOptions(getImportValues(), "import.pl");
+	    schedule_options = new ScriptOptions(getScheduleValues(), "schedule.pl");
+	}
+    }
+    
+    /** Destructor.
+     * @see org.greenstone.gatherer.Configuration
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.cdm.CollectionDesignManager
+     */
+    public void destroy() {
+	cdm.destroy();
+	Configuration.setCollectionConfiguration(null);
+	// this is called after calling collection.destroy, so we shouldn't need it here too.
+// 	if (Gatherer.g_man != null) {
+// 	    Gatherer.g_man.updateUI();
+// 	}
+	cdm = null;
+	document = null;
+    }
+    
+    /** Determine the number of documents and folders in this collection. */
+    public int getCount() {
+	return getCount((TreeNode)Gatherer.c_man.getCollectionTreeModel().getRoot(), true, true);
+    }
+    
+    /** Calculates the number of documents in this collection. */
+    public int getDocumentCount() {
+	return getCount((TreeNode)Gatherer.c_man.getCollectionTreeModel().getRoot(), false, true);
+    }
+    
+    /** Retrieve the description of this collection.
+     * @return a String
+     */
+    public String getDescription() {
+	if(cdm == null) {
+	    return StaticStrings.EMPTY_STR;
+	}
+	CollectionMeta collection_extra_collectionmeta = cdm.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_COLLECTIONEXTRA_STR);
+	return collection_extra_collectionmeta.getValue(CollectionMeta.TEXT);
+    }
+    
+    /** Retrieve the authors email for this collection.
+     * @return The email as a <strong>String</strong>.
+     */
+    public String getEmail() {
+	if(cdm == null) {
+	    return StaticStrings.EMPTY_STR;
+	}
+	CollectionMeta creator_collectionmeta = new CollectionMeta(CollectionDesignManager.collect_config.getCreator());
+	return creator_collectionmeta.getValue(CollectionMeta.TEXT);
+    }
+
+    /** Retrieve the short name for this collection.
+     * @return The name as a <strong>String</strong>.
+     */
+    public String getName() {
+	return getCollectionTailName();
+    }
+
+    /** @return just the tail-name of the collection: without any collection group prefix */
+    public String getCollectionTailName() {
+	return file.getParentFile().getName();
+    }
+
+    /** @return just the name of the collection group if any, else returns "" */
+    public String getCollectionGroupName() {
+	String groupAndColName = getGroupQualifiedName(true);
+
+	int slash = groupAndColName.indexOf('/');
+	if(slash == -1) {
+	    return "";
+	} else { // remove the colName from the end, to get the groupName
+	    return groupAndColName.substring(0, slash);
+	}
+    }
+
+    /** 
+     * If the collection is part of a collection group, returns the 
+     * colgroupname/colname, else returns the colname.
+     * If url = true, then returns the sub-path as a URL (containing / only), 
+     * and if url = false, then the sub-path is returned in filepath form 
+     * (\ or /, depending on the OS).
+     * @return collection-group-name/collection-name
+     */
+    public String getGroupQualifiedName(boolean url) {
+	// Subtracting collect dir's path from the current collection's path
+
+	String groupAndCol;
+	String collectDir = Gatherer.getCollectDirectoryPath();
+	String currentCollDir = file.getParentFile().getAbsolutePath(); 
+   
+	if(currentCollDir.startsWith(collectDir)) {
+	    groupAndCol = currentCollDir.substring(collectDir.length());	    
+	} else {
+	    // shouldn't happen, but in case it does, just return the collection name.
+	    System.err.println("Current collection " + currentCollDir + " is not located inside collectdir " + collectDir);
+	    groupAndCol = getCollectionTailName();
+	}
+
+	if(url) {
+	    groupAndCol = groupAndCol.replace('\\', '/');
+	}
+
+	return groupAndCol;
+    }
+    
+    public File getCollectionDirectory() {
+	return file.getParentFile();
+    }
+
+    // return full file path of the collection object
+    public String getCollectionPath() {
+	return file.getAbsolutePath();
+    }
+
+    /** Determine if this collection has been saved since the last major change.
+     * @return <i>true</i> if it has been saved recently, <i>false</i> otherwise.
+     */
+    public boolean getSaved() {
+	return saved;
+    }
+
+    /** Determine if this collection has had its collection files changed since the last build.
+     * @return <i>true</i> if they have been saved, <i>false</i> otherwise.
+     */
+    public boolean getFilesChanged() {
+	return filesChanged;
+    }
+    
+    /** Set the flag that marks weather or not the files has been changed since the last build.
+	@param changed if files has been added/removed, as a <strong>boolean</strong>
+    */
+    public void setFilesChanged(boolean changed) {
+	filesChanged = changed;
+    }
+
+    /** Determine if this collection has had metadata changed since the last build.
+     * @return <i>true</i> if it has been changed, <i>false</i> otherwise.
+     */
+    public boolean getMetadataChanged() {
+	return metadataChanged;
+    }
+    
+    /** Set the flag that marks weather or not the metadata has been changed since the last build.
+	@param changed if metadata has been added/changed/removed, as a <strong>boolean</strong>
+    */
+    public void setMetadataChanged(boolean changed) {
+	metadataChanged = changed;
+    }
+
+    /** Retrieve the title of this collection.
+     * @return The title as a <strong>String</strong>.
+     */
+    public String getTitle() {
+	if(cdm == null) {
+	    return StaticStrings.EMPTY_STR;
+	}
+	CollectionMeta collection_name_collectionmeta = cdm.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_COLLECTIONNAME_STR);
+	return collection_name_collectionmeta.getValue(CollectionMeta.TEXT);
+    }
+
+    /** Save this xml document to the given file. */
+    public void save() {
+	XMLTools.writeXMLFile(file, document);
+	saved = true;
+    }
+
+    public void setBaseCollection(String base_collection) {
+	set(BASE_COLLECTION, base_collection);
+    }
+    
+    /** Set the value of imported to the given value.
+     * @param value The new value for imported, <i>true</i> if the collection has been imported successfully, <i>false</i> otherwise.
+     */
+    public void setImported(boolean value) {
+	set(IMPORTED, value);
+	saved = false;
+    }
+
+    /** Set the value of saved to the given value.
+     * @param value The new value for saved, <i>true</i> if the collection has been saved recently, <i>false</i> otherwise.
+     */
+    public void setSaved(boolean value) {
+	saved = value;
+    }
+
+    /** Set the value of title to the given value.
+     * @param title The new <strong>String</strong> title.
+     */
+    public void setTitle(String title) {
+	if(cdm != null) {
+	    CollectionMeta collection_name_collectionmeta = cdm.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_COLLECTIONNAME_STR);
+	    collection_name_collectionmeta.setValue(title);
+	}
+    }
+
+    /** Method called to return a textual representation of a class, which in this case is the collections title.
+     * @return A <strong>String</strong> containing the collections title.
+     */
+    public String toString() {
+	return getTitle();
+    }
+ 
+    public boolean toSkimFile(){
+	 Element document_element = document.getDocumentElement();
+	 NodeList arguments = document_element.getElementsByTagName(ARGUMENT);
+          for(int i = 0;  i < arguments.getLength(); i++) {
+	      Element argument_element = (Element) arguments.item(i);
+              if(argument_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equalsIgnoreCase(SKIM_FILE)) {
+		  String skimfile = XMLTools.getValue(argument_element); 
+		  if (skimfile !=null && skimfile.trim().toLowerCase().equals("false")){
+		      return false;
+		  } 
+	      }
+	     
+	  }
+	  
+	  return true;
+    }
+
+    
+    /** Get the value of a collection argument. */
+    private boolean get(String name) {
+	boolean result = false;
+	try {
+	    Element document_element = document.getDocumentElement();
+	    NodeList arguments = document_element.getElementsByTagName(ARGUMENT);
+	    boolean found = false;
+	    for(int i = 0; !found && i < arguments.getLength(); i++) {
+		Element argument_element = (Element) arguments.item(i);
+		if(argument_element.getParentNode() == document_element) {
+		    if(argument_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equalsIgnoreCase(name)) {
+			String value = XMLTools.getValue(argument_element);
+			if(value.equalsIgnoreCase(StaticStrings.TRUE_STR)) {
+			    result = true;
+			}
+			found = true;
+			value = null;
+		    }
+		}
+		argument_element = null;
+	    }
+	    arguments = null;
+	    document_element = null;
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+	return result;
+    }
+    
+    /** Get the value of a collection argument. */
+    private String getString(String name) {
+	String result = "";
+	try {
+	    Element document_element = document.getDocumentElement();
+	    NodeList arguments = document_element.getElementsByTagName(ARGUMENT);
+	    boolean found = false;
+	    for(int i = 0; !found && i < arguments.getLength(); i++) {
+		Element argument_element = (Element) arguments.item(i);
+		if(argument_element.getParentNode() == document_element) {
+		    if(argument_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equalsIgnoreCase(name)) {
+			result = XMLTools.getValue(argument_element);
+			found = true;
+		    }
+		}
+		argument_element = null;
+	    }
+	    arguments = null;
+	    document_element = null;
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+	return result;
+    }
+
+    
+
+    /** Method to retrieve the current import/build/export/whatever options associated with this Collection. */
+    private Element getValues(String val_type) {
+	Element values_element = null;
+	try {
+	    Element document_element = document.getDocumentElement();
+	    Element config_element = (Element) XMLTools.getNodeFromNamed(document_element, BUILD_CONFIG);
+	    values_element = (Element) XMLTools.getNodeFromNamed(config_element, val_type);
+	    config_element = null;
+	    document_element = null;
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+	return values_element;
+    }
+
+    
+    /** Method to retrieve the current build options associated with this Collection. */
+    private Element getBuildValues() {
+	Element build_values_element = null;
+	try {
+	    Element document_element = document.getDocumentElement();
+	    Element build_config_element = (Element) XMLTools.getNodeFromNamed(document_element, BUILD_CONFIG);
+	    build_values_element = (Element) XMLTools.getNodeFromNamed(build_config_element, BUILD);
+	    build_config_element = null;
+	    document_element = null;
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+	return build_values_element;
+    }
+
+    private Element getScheduleValues() {
+	Element schedule_values_element = null;
+	try {
+	    Element document_element = document.getDocumentElement();
+	    Element build_config_element = (Element) XMLTools.getNodeFromNamed(document_element, BUILD_CONFIG);
+	    schedule_values_element = (Element) XMLTools.getNodeFromNamed(build_config_element, SCHEDULE);
+	    build_config_element = null;
+	    document_element = null;
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	} 
+	return schedule_values_element;
+    }
+    
+    /** Count either documents or folders, depending on the state of the given boolean. */
+    private int getCount(TreeNode node, boolean count_folders, boolean count_files) {
+	int count = 0;
+	File file = ((CollectionTreeNode)node).getFile();
+	if(file.isFile() && count_files) {
+	    count++;
+	}
+	else if(file.isDirectory() && count_folders) {
+	    count++;
+	}
+	for(int i = 0; !file.getName().equals("CVS") && i < node.getChildCount(); i++) {
+	    count = count + getCount(node.getChildAt(i), count_folders, count_files);
+	}
+	return count;
+    }
+    
+    /** Method to retrieve the current import options associated with this Collection. */
+    public Element getImportValues() {
+	Element import_values_element = null;
+	try {
+	    Element document_element = document.getDocumentElement();
+	    Element build_config_element = (Element) XMLTools.getNodeFromNamed(document_element, BUILD_CONFIG);
+	    import_values_element = (Element) XMLTools.getNodeFromNamed(build_config_element, IMPORT);
+	    build_config_element = null;
+	    document_element = null;
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+	return import_values_element;
+    }
+    
+    /** Set the value of a collection argument. */
+    private void set(String name, boolean value) {
+	set(name, (value ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+    }
+    
+    private void set(String name, String value) {
+	try {
+	    Element document_element = document.getDocumentElement();
+	    NodeList arguments = document_element.getElementsByTagName(ARGUMENT);
+	    boolean found = false;
+	    for(int i = 0; !found && i < arguments.getLength(); i++) {
+		Element argument_element = (Element) arguments.item(i);
+		if(argument_element.getParentNode() == document_element) {
+		    if(argument_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equalsIgnoreCase(name)) {
+			// Strip any current value nodes.
+			while(argument_element.hasChildNodes()) {
+			    argument_element.removeChild(argument_element.getFirstChild());
+			}
+			// Append new value
+			argument_element.appendChild(document.createTextNode(value));
+			found = true;
+		    }
+		}
+		argument_element = null;
+	    }
+	    // Append it
+	    if(!found) {
+		Element argument_element = document.createElement(ARGUMENT);
+		argument_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, name);
+		argument_element.appendChild(document.createTextNode(value));
+		document_element.appendChild(argument_element);
+		argument_element = null;
+	    }
+	    arguments = null;
+	    document_element = null;
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionContentsChangedListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionContentsChangedListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionContentsChangedListener.java	(revision 31635)
@@ -0,0 +1,37 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.collection;
+
+
+import java.io.File;
+
+
+public interface CollectionContentsChangedListener
+{
+    /** Notification that a new file has been added to the collection. */
+    public void fileAddedToCollection(File file);
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionManager.java	(revision 31635)
@@ -0,0 +1,2426 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.collection;
+
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.filechooser.FileSystemView;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.cdm.CollectionMeta;
+import org.greenstone.gatherer.cdm.CollectionMetaManager;
+import org.greenstone.gatherer.cdm.CommandTokenizer;
+import org.greenstone.gatherer.cdm.BuildTypeManager;
+import org.greenstone.gatherer.cdm.CollectionConfiguration;
+import org.greenstone.gatherer.greenstone.Classifiers;
+import org.greenstone.gatherer.greenstone.LocalGreenstone;
+import org.greenstone.gatherer.greenstone.LocalLibraryServer;
+import org.greenstone.gatherer.greenstone.Plugins;
+import org.greenstone.gatherer.greenstone3.ServletConfiguration;
+import org.greenstone.gatherer.gui.LockFileDialog;
+import org.greenstone.gatherer.gui.ModalProgressPopup;
+import org.greenstone.gatherer.gui.WarningDialog;
+import org.greenstone.gatherer.metadata.DocXMLFileManager;
+import org.greenstone.gatherer.metadata.FilenameEncoding;
+import org.greenstone.gatherer.metadata.MetadataChangedListener;
+import org.greenstone.gatherer.metadata.MetadataSet;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.metadata.MetadataXMLFileManager;
+import org.greenstone.gatherer.metadata.ProfileXMLFileManager;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.shell.GShellEvent;
+import org.greenstone.gatherer.shell.GShellListener;
+import org.greenstone.gatherer.shell.GShellProgressMonitor;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+import org.greenstone.gatherer.util.GS3ServerThread;
+
+/** This class manages many aspects of the collection, from its creation via scripts, data access via methods and its importing and building into the final collection. It is also responsible for firing appropriate event when significant changes have occured within the collection, and for creating a new metadata set manager as necessary.
+ * @author John Thompson
+ * @version 2.3
+ */
+public class CollectionManager
+    implements GShellListener, MetadataChangedListener
+{
+    /** Are we currently in the process of building? */
+    static private boolean building = false;
+    /** Are we currently in the process of importing? */
+    static private boolean importing = false;
+    /** Are we currently in the process of scheduling? */ 
+    static private boolean scheduling = false; 
+    /** The objects listening for CollectionContentsChanged events. */
+    static private ArrayList collection_contents_changed_listeners = new ArrayList();
+    /** The collection this manager is managing! */
+    static private Collection collection = null;
+    /** The collection tree (used in both Gather and Enrich panes). */
+    static private CollectionTree collection_tree = null;
+    /** The collection tree model. */
+    static private CollectionTreeModel collection_tree_model = null;
+    /** An inner class listener responsible for noting tree changes and resetting saved when they occur. */
+    static private FMTreeModelListener fm_tree_model_listener = null;
+    /** The monitor responsible for parsing the build process. */
+    static private GShellProgressMonitor build_monitor = null;
+    /** The monitor responsible for parsing the import process. */
+    static private GShellProgressMonitor import_monitor = null;
+    /** The monitor responsible for parsing the scheduler process. */ 
+    static private GShellProgressMonitor schedule_monitor = null; 
+    ///** The monitor responsible for parsing the fedora collection delete process. */ 
+    //static private GShellProgressMonitor fedora_coldelete_monitor = null;
+
+    static private String delete_collection_name = null;
+
+    /** The name of the standard lock file. */
+    static final public String LOCK_FILE = "gli.lck";
+
+    /** Used to indicate the source of the message is the file collection methods. */
+    static final public int COLLECT   = 3;
+    /** Used to indicate the source of the message is the building methods. */
+    static final public int BUILDING  = 5;
+    /** Used to indicate the source of the message is in the scheduling methods...?  */ 
+    static final public int SCHEDULING = 7; 
+
+    /** To store the path to the perl scripts. In the case of local Greenstone servers, 
+     * this will be the local bin/script folder. */
+    static private String scriptPath = "";
+  
+    /** Constructor. */
+    public CollectionManager() {
+	// Initialisation.
+	this.building = false;
+	this.importing = false;
+	this.scheduling = false; 
+	this.collection = null;
+
+	MetadataXMLFileManager.addMetadataChangedListener(this);
+
+	// If using a remote Greenstone server, delete the local collect directory because it will be out of date
+	if (Gatherer.isGsdlRemote) {
+	    System.err.println("Deleting user's local collect directory...");
+	    Utility.delete(new File(Gatherer.getCollectDirectoryPath()));
+	    System.err.println("Done.");
+	    new File(Gatherer.getCollectDirectoryPath()).mkdirs();
+
+	    scriptPath = ""; // remote greenstone: scriptPath will be determined on remote server side
+	} 
+	else { // local greenstone case: scripts are inside bin/script
+	    scriptPath = LocalGreenstone.getBinScriptDirectoryPath();
+	}
+    }
+
+
+    static public void addCollectionContentsChangedListener(CollectionContentsChangedListener listener)
+    {
+	collection_contents_changed_listeners.add(listener);
+    }
+
+
+    /** This method calls the builcol.pl scripts via a GShell so as to not lock up the processor.
+     * @see org.greenstone.gatherer.Configuration
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.Collection
+     * @see org.greenstone.gatherer.gui.BuildOptions
+     * @see org.greenstone.gatherer.shell.GShell
+     * @see org.greenstone.gatherer.shell.GShellListener
+     * @see org.greenstone.gatherer.shell.GShellProgressMonitor
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    public void buildCollection()
+    {
+    
+	DebugStream.println("In CollectionManager.buildCollection(), CollectionDesignManager.isCompleteBuild(): " + CollectionDesignManager.isCompleteBuild());
+	DebugStream.println("Is event dispatch thread: " + SwingUtilities.isEventDispatchThread());
+	building = true;
+
+	// Generate the buildcol.pl command
+	ArrayList command_parts_list = new ArrayList();
+	if (!Gatherer.isGsdlRemote) {
+	    command_parts_list.add(Configuration.perl_path);
+	    command_parts_list.add("-S");
+	}
+
+	if (Configuration.fedora_info.isActive()) {
+	    command_parts_list.add(scriptPath + "g2f-buildcol.pl");
+
+	    command_parts_list.add("-hostname");
+	    command_parts_list.add(Configuration.fedora_info.getHostname());
+
+	    command_parts_list.add("-port");
+	    command_parts_list.add(Configuration.fedora_info.getPort());
+
+	    command_parts_list.add("-username");
+	    command_parts_list.add(Configuration.fedora_info.getUsername());
+
+	    command_parts_list.add("-password");
+	    command_parts_list.add(Configuration.fedora_info.getPassword());
+
+	    command_parts_list.add("-protocol");
+	    command_parts_list.add(Configuration.fedora_info.getProtocol());
+
+	}
+	else {
+		
+		if ( !CollectionDesignManager.isCompleteBuild() && CollectionDesignManager.index_manager.isLucene() ) {
+			command_parts_list.add(scriptPath + "incremental-buildcol.pl");
+			CollectionDesignManager.setBuildcolWasFull(false);
+		} else {
+			command_parts_list.add(scriptPath + "full-buildcol.pl");
+			CollectionDesignManager.setBuildcolWasFull(true);
+		}
+	}
+
+	command_parts_list.add("-gli");
+	command_parts_list.add("-language");
+	command_parts_list.add(Configuration.getLanguage());
+
+	// For now, for solr collections, we pass in -activate
+	// but GLI won't do moving building to index
+	
+	//if(CollectionManager.isSolrCollection()) {
+	    command_parts_list.add("-activate");
+	    //}
+
+	if(Gatherer.GS3) {
+	    command_parts_list.add("-site");
+	    command_parts_list.add(Configuration.site_name);
+	}
+
+	if(!Gatherer.isGsdlRemote) {
+	    command_parts_list.add("-collectdir");
+	    command_parts_list.add(getCollectDirectory()); // <../collect/>
+	}
+
+	String[] build_options = collection.build_options.getValues();
+	for (int i = 0; i < build_options.length; i++) {
+	    command_parts_list.add(build_options[i]);
+	}
+
+	command_parts_list.add(collection.getGroupQualifiedName(false)); // (colgroup/)colname
+
+	// Run the buildcol.pl and
+	String[] command_parts = (String[]) command_parts_list.toArray(new String[0]);
+	GShell shell = new GShell(command_parts, GShell.BUILD, BUILDING, this, build_monitor, GShell.GSHELL_BUILD);
+	shell.addGShellListener(Gatherer.g_man.create_pane);
+	shell.addGShellListener(Gatherer.g_man.format_pane);
+	shell.start();
+	
+    }
+
+    /*probably repeating alot of work, but I want to keep this separate... wendy*/
+    public void scheduleBuild()
+    {
+	DebugStream.println("In CollectionManager.scheduleBuild(), CollectionDesignManager.isCompleteBuild(): " + CollectionDesignManager.isCompleteBuild()); 
+	DebugStream.println("Is event dispatch threa: " + SwingUtilities.isEventDispatchThread()); 
+
+	ArrayList sched_list = new ArrayList(); 
+	if (!Gatherer.isGsdlRemote) {
+	    sched_list.add(Configuration.perl_path);
+	    sched_list.add("-S");
+	}
+	sched_list.add(scriptPath + "schedule.pl"); 
+	sched_list.add("-colname"); 
+	sched_list.add(collection.getName()); 
+	sched_list.add("-gli"); 
+
+	// First, generate the import.pl command, also converting to a string
+	// Generate the import.pl command
+	ArrayList import_list = new ArrayList();
+	if (!Gatherer.isGsdlRemote) {
+	    import_list.add(Configuration.perl_path);
+	    import_list.add("-S");
+	}
+
+	String cmdPrefix = CollectionDesignManager.isCompleteBuild() ? "full-" : "incremental-";
+	import_list.add(scriptPath + cmdPrefix + "import.pl");
+	import_list.add("-language");
+	import_list.add(Configuration.getLanguage());
+
+	if(Gatherer.GS3) {
+	    import_list.add("-site");
+	    import_list.add(Configuration.site_name);
+	}
+	
+	if(!Gatherer.isGsdlRemote) {
+	    import_list.add("-collectdir");
+	    import_list.add(getCollectDirectory());
+	}
+
+	String[] import_options = collection.import_options.getValues();
+	int i = 0; 
+	for (i = 0; i < import_options.length; i++) {
+	    import_list.add(import_options[i]);
+	}
+
+	import_list.add(collection.getGroupQualifiedName(false)); // (colgroup/)colname
+
+	String[] import_parts = (String[]) import_list.toArray(new String[0]); 
+	String command = "";
+	i = 0; 
+	for (i = 0; i < import_parts.length-1; i++) {
+	    command = command + import_parts[i] + " "; 
+	} 
+	command = command + import_parts[i]; 
+
+	sched_list.add("-import"); 
+	sched_list.add("\"" + command + "\""); 
+
+	// Generate the buildcol.pl command, also converting to a string
+	ArrayList build_list = new ArrayList();
+
+	// i'm not doing this in schedule.pl right now - should i be? 
+	if (!Gatherer.isGsdlRemote) {
+	    build_list.add(Configuration.perl_path);
+	    build_list.add("-S");
+	}
+
+	String buildType = (new CollectionMeta( CollectionDesignManager.collect_config.getBuildType() )).getValue(CollectionMeta.TEXT);
+	if ( !CollectionDesignManager.isCompleteBuild() && buildType.equals( BuildTypeManager.BUILD_TYPE_LUCENE ) ) {
+		build_list.add(scriptPath + "incremental-buildcol.pl");
+	} else {
+		build_list.add(scriptPath + "full-buildcol.pl");
+	}
+
+	build_list.add("-language");
+	build_list.add(Configuration.getLanguage());
+
+	if(Gatherer.GS3) {
+	    build_list.add("-site");
+	    build_list.add(Configuration.site_name);
+	}
+
+	if(!Gatherer.isGsdlRemote) {
+	    build_list.add("-collectdir");
+	    build_list.add(getCollectDirectory());
+	}
+
+	String[] build_options = collection.build_options.getValues();
+	for (i = 0; i < build_options.length; i++) {
+	    build_list.add(build_options[i]);
+	}
+
+	build_list.add(collection.getGroupQualifiedName(false)); // (colgroup/)colname
+	
+	//build actual string
+	String[] build_parts = (String[]) build_list.toArray(new String[0]);
+	String command2 = "";
+        for(i = 0; i < build_parts.length-1; i++) {
+            command2 = command2 + build_parts[i] + " ";
+        }
+	command2 = command2 + build_parts[i]; 
+
+	sched_list.add("-build"); 
+	sched_list.add("\"" + command2 + "\""); 
+	
+	//next, the scheduling frequency goes here
+	String[] schedule_options = collection.schedule_options.getValues();  
+	for(i = 0; i < schedule_options.length; i++) { 
+	    sched_list.add(schedule_options[i]); 
+	}
+
+	//now, hope it will run. ;) 
+	String[] sched_parts = (String[]) sched_list.toArray(new String[0]); 
+
+    	GShell shell = new GShell(sched_parts, GShell.SCHEDULE, SCHEDULING, this, schedule_monitor, GShell.GSHELL_SCHEDULE);
+	shell.addGShellListener(Gatherer.g_man.create_pane);
+	shell.addGShellListener(Gatherer.g_man.format_pane);
+	shell.start();
+    }
+
+    /** Used to determine whether the currently active collection has been built.
+     * @return A boolean indicating the built status of the collection.
+     */
+    public boolean built() {
+	if(collection != null) {
+	    // Determine if the collection has been built by looking for the build.cfg (gs2) 
+	    // buildConfig.xml (gs3) or export.inf (fedora) file
+	    String file_name = "";
+	    
+	    if (Configuration.fedora_info != null && Configuration.fedora_info.isActive()) { // FLI case
+		// Fedora build
+		//file_name = getLoadedCollectionArchivesDirectoryPath() + "import.inf";
+		//file_name = getLoadedCollectionExportDirectoryPath() + "export.inf"; // export.pl no longer generates this
+		file_name = getLoadedCollectionExportDirectoryPath() + "archiveinf-doc.gdb";
+	    } else { 
+		// GLI is running, check if it's greenstone 3 or greenstone 2
+		if (Gatherer.GS3) { // GS3 GLI
+		    file_name = getLoadedCollectionIndexDirectoryPath() + Utility.BUILD_CONFIG_XML;
+		}
+		else { // greenstone 2 GLI
+		    file_name = getLoadedCollectionIndexDirectoryPath() + Utility.BUILD_CFG;		   
+		}
+	    }
+	    File test_file = new File(file_name);
+	    return test_file.exists();
+	}
+	return false;
+    }
+
+    /** Used to determine whether the currently active collection has been imported.
+     * @return A boolean indicating the imported status of the collection.
+     */
+    public boolean imported() {
+        if ( collection != null ) {
+            String file_name = getLoadedCollectionDirectoryPath() + "archives";
+            File test_file = new File(file_name);
+            return test_file.exists();
+        }
+        return false;
+    }
+
+    /** a test method to see if we can delete a directory/file - returns false is the file or any of the contents of a directory cannot be deleted */
+    static private boolean canDelete(File file)
+    {
+	if (!file.isDirectory()) {
+	    return file.canWrite();
+	}
+	File [] file_list = file.listFiles();
+	for (int i=0; i<file_list.length; i++) {
+	    if (!canDelete(file_list[i])) {
+		return false;
+	    }
+	}
+	return true;
+    }
+
+
+    /** Called to close the current collection and remove its lock file.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.Collection
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    public void closeCollection() {
+	if (collection == null) {
+	    return;
+	}
+	DebugStream.println("Close collection: " + collection.getName());
+
+	// Remove the lock on this file, then remove the collection.
+	File lock_file = new File(getLoadedCollectionDirectoryPath() + LOCK_FILE);
+	lock_file.delete();
+	if (lock_file.exists()) {
+	    System.err.println("Warning: Lockfile was not successfully deleted.");
+	}
+
+	// Remove the lock file on the server
+	if (Gatherer.isGsdlRemote) {
+	    Gatherer.remoteGreenstoneServer.deleteCollectionFile(collection.getGroupQualifiedName(false), lock_file);
+	}
+	
+	//  release the current build_log file - else we're unable to delete the last closed collection 
+	// until another collection is opened and its opening a new build_log closes this old one
+	Gatherer.g_man.create_pane.options_pane.closeCurrentLogDocument();
+
+	MetadataSetManager.clearMetadataSets();
+	MetadataXMLFileManager.clearMetadataXMLFiles();
+	DocXMLFileManager.clearDocXMLFiles();
+	ProfileXMLFileManager.clearProfileXMLFile();
+	
+	collection.destroy();
+	collection = null;
+	collection_tree_model = null;
+	//Configuration.setCollectionConfiguration(null);
+	Gatherer.refresh(Gatherer.COLLECTION_CLOSED);
+	if (Gatherer.g_man != null) {
+	    Gatherer.g_man.updateUI();  // !!! Necessary?
+	}
+    }
+
+//This method is no longer used in gs3 since the modification of CollectionConfiguration.java
+//    public void convertToGS3Collection() {
+//	// Generate the convert_coll_from_gs2.pl command
+//	ArrayList command_parts_list = new ArrayList();
+//	if ((Utility.isWindows()) && (!Gatherer.isGsdlRemote)) {
+//	    command_parts_list.add(Configuration.perl_path);
+//	    command_parts_list.add("-S");
+//	}
+//	command_parts_list.add(Configuration.getGS3ScriptPath() + "convert_coll_from_gs2.pl");
+//
+//	command_parts_list.add("-site");
+//	command_parts_list.add(Configuration.site_name);
+//
+//	command_parts_list.add("-collectdir");
+//	command_parts_list.add(getCollectDirectory());
+//    	command_parts_list.add(collection.getGroupQualifiedName(false)); // (colgroup/)colname
+//
+//	// Run the convert_coll_from_gs2.pl command
+//	String[] command_parts = (String[]) command_parts_list.toArray(new String[0]);
+//	GShell process = new GShell(command_parts, GShell.CONVERT, COLLECT, this, null, GShell.GSHELL_CONVERT);
+//	process.addGShellListener(this);
+//	process.run(); // Don't bother threading this... yet
+//       
+//    }
+
+    /** When basing a new collection on an existing one, we need to copy 
+     *	over some extra directories: all except import, archives, building, index 
+     * really we just want images, macros, perllib, but there may also be eg style, or other dirs.
+     */
+    private boolean copyExtraBaseCollStuff(File new_coll_dir, File base_coll_dir) {
+	if (!new_coll_dir.isDirectory() || !base_coll_dir.isDirectory()) {
+	    return false;
+	}
+	DebugStream.println("Copying extra dirs from the base collection");
+	
+	
+	File subdirs[] = base_coll_dir.listFiles();
+	for (int i = 0; subdirs != null && i < subdirs.length; i++) {
+	  File subdir = subdirs[i];
+	  if (subdir.isDirectory()) {
+	    String dir_name = subdir.getName();
+	    // ignore those we don't need, (archives, buildng, index) and 
+	    // those we are handling in another place (import, etc, metadata)
+	    if (dir_name.startsWith ("import") || dir_name.startsWith("archives") || dir_name.startsWith("building") || dir_name.startsWith("index") || dir_name.startsWith("etc") || dir_name.startsWith("metadata") || dir_name.startsWith("log") || dir_name.startsWith("tmp")) {
+	      continue;
+	    }
+	    try {
+	      // copy the directory
+	      File new_coll_subdir = new File(new_coll_dir, dir_name);
+	      new_coll_subdir.mkdirs();
+	      // Copy with force overwrite, since it's a new collection that's currently
+	      // being created and won't have anything in it but at most modelcol stuff
+	      Gatherer.f_man.getQueue().copyDirectoryContents(subdir, new_coll_subdir, true);
+	    }
+	    catch (Exception e) {
+	      DebugStream.println("Couldn't copy over the" + subdir+" dir from the base collection: "+e.toString());
+	    }
+	  }
+	}
+
+	return true;
+    }
+  
+    /** Used to set the current collection to the given collection. Note that this call should -always- be proceeded by a ready call, and if the collection is ready and the saved flag is unset then the user should be prompted to save. Also note that this method creates yet another GShell to run buildcol.pl.
+     * @param description a description of the collection as a String
+     * @param email the email address of the author/maintainer as a String
+     * @param name the short name of the collection, which will subsequently be used to refer to this particular collection, as a String
+     * @param title the longer title of the collection as a String
+     * @param base_collection_directory if the user has chosen to base their new collection on an existing one, this is the directory where this base collection can be found, as a File, otherwise its null
+     * @param metadata_sets if the user has decided to select several metadata sets with which to initially populate the GLI then this is an ArrayList of metadata set file names, otherwise its null
+     */
+    public void createCollection(String description, String email, String name, String title, File base_collection_directory, ArrayList metadata_sets)
+    {
+	// Display a modal progress popup to indicate that the collection is being loaded
+	ModalProgressPopup create_collection_progress_popup = new ModalProgressPopup(Dictionary.get("CollectionManager.Creating_Collection"), Dictionary.get("CollectionManager.Creating_Collection_Please_Wait"));
+	create_collection_progress_popup.display();
+
+	// Create the collection on a separate thread so the progress bar updates correctly
+	(new CreateCollectionTask(description, email, name, title, base_collection_directory, metadata_sets, create_collection_progress_popup)).start();
+    }
+
+
+    private class CreateCollectionTask
+	extends Thread
+    {
+	private String description = null;
+	private String email = null;
+	private String name = null;
+	private String title = null;
+	private File base_collection_directory = null;
+	private ArrayList metadata_sets = null;
+	private ModalProgressPopup create_collection_progress_popup = null;
+
+	public CreateCollectionTask(String description, String email, String name, String title, File base_collection_directory, ArrayList metadata_sets, ModalProgressPopup create_collection_progress_popup)
+	{
+	    this.description = description;
+	    this.email = email;
+	    this.name = name;
+	    this.title = title;
+	    this.base_collection_directory = base_collection_directory;
+	    this.metadata_sets = metadata_sets;
+	    this.create_collection_progress_popup = create_collection_progress_popup;
+	}
+
+	public void run()
+	{
+	    createCollectionInternal(description, email, name, title, base_collection_directory, metadata_sets);
+	    create_collection_progress_popup.close();
+	}
+    }
+
+
+    private void createCollectionInternal(String description, String email, String name, String title, File base_collection_directory, ArrayList metadata_sets)
+    {
+	try {
+	    // first make sure that the collect directory exists
+	    File collect_dir = new File(getDefaultCollectDirectory());
+	    if (!collect_dir.exists()) {
+		collect_dir.mkdirs();
+	    }
+
+	    // Create the new collection
+	    makeCollection(name, email);
+
+	    // Check that the collection has been created successfully
+	    String collection_directory_path = getCollectionDirectoryPath(name);
+	    if (!new File(collection_directory_path).exists()) {
+		// If there is no collection directory then the creation was unsuccessful, or cancelled
+	      
+		return;
+	    }
+
+	    // Check for the existence of the collection configuration file
+	    String file_name = ((Gatherer.GS3 == true)? Utility.COLLECTION_CONFIG_XML : Utility.COLLECT_CFG);
+	    File collect_cfg_file = new File(collection_directory_path + "etc" + File.separator + file_name);
+
+	    if (!collect_cfg_file.exists()) {
+		System.err.println("Error: no " + file_name + " file has been created!");
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Cannot_Create_Collection_With_Reason", Dictionary.get("CollectionManager.No_Config_File")), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE); 
+		return;
+	    }
+ 	    
+	    // ACTIVE_DIR/log/
+	    File log_dir = new File(collection_directory_path + "log");
+	    log_dir.mkdirs();
+
+	    // Make sure an import folder exists
+	    File collection_import_directory = new File(collection_directory_path + "import");
+	    if (!collection_import_directory.exists()) {
+		collection_import_directory.mkdirs();
+		if (Gatherer.isGsdlRemote) {
+		    Gatherer.remoteGreenstoneServer.newCollectionDirectory(name, collection_import_directory);
+		}
+	    }
+
+	    // Now create the collection object around the directory.
+	    collection = new Collection(new File(collection_directory_path, "gli.col"));
+
+	    // for remote case, scheduling causes an Exception on creating a new collection that 
+	    // can't be recovered from. For GS3, it doesn't work since it it trying to access etc/main.cfg
+	    if (canDoScheduling()) {
+		scheduling();
+	    }
+	    	    
+	    MetadataSetManager.clearMetadataSets();
+	    MetadataXMLFileManager.clearMetadataXMLFiles();
+	    DocXMLFileManager.clearDocXMLFiles();
+
+	    // Import default metadata sets, if any
+	    // for (int i = 0; metadata_sets != null && i < metadata_sets.size(); i++) {
+	    // importMetadataSet((MetadataSet) metadata_sets.get(i));
+	    // }
+
+	    ProfileXMLFileManager.loadProfileXMLFile(new File(collection_directory_path + "metadata"));
+
+	    // Before creating the CollectionDesignManager check if we are basing it upon some other collection
+	    if (base_collection_directory != null) {
+		DebugStream.println("Basing new collection on existing one: " + base_collection_directory);
+
+		// If we're using a remote Greenstone server, download the collection shell to get the files needed
+		if (Gatherer.isGsdlRemote) {
+		    String base_collection_name = base_collection_directory.getName();
+		    Gatherer.remoteGreenstoneServer.downloadCollection(base_collection_name);
+		}
+
+		collection.setBaseCollection(base_collection_directory.getAbsolutePath());
+		// copy over other needed directories
+		copyExtraBaseCollStuff(new File(collection_directory_path), base_collection_directory);
+
+		// Try to import any existing metadata sets for this collection
+		// Look in base_collection_directory/metadata and import any metadata sets found.
+		File base_metadata_directory = new File(base_collection_directory, "metadata");
+		ArrayList base_metadata_sets = MetadataSetManager.listMetadataSets(base_metadata_directory);
+		if (base_metadata_sets != null) {
+		    for (int i = 0; i < base_metadata_sets.size(); i++) {
+			importMetadataSet((MetadataSet) base_metadata_sets.get(i));
+		    }
+		}
+		else {
+		    DebugStream.println("This base collection has no metadata directory.");
+		}
+
+		// Now we update our collect.cfg
+		DebugStream.println("Copy and update " + file_name + " from base collection.");
+
+		if (Gatherer.GS3 == true) {
+		    updateCollectionConfigXML(new File(base_collection_directory, Utility.CONFIG_GS3_FILE),
+					new File(collection_directory_path, Utility.CONFIG_GS3_FILE)); 
+		} else {
+		    updateCollectionCFG(new File(base_collection_directory, Utility.CONFIG_FILE),
+					new File(collection_directory_path, Utility.CONFIG_FILE),
+					description, email, title);
+		}
+	    }
+	    else {
+	      // only load metadata sets here if we have not based the collection on any other.
+	      // Load the default metadata sets
+	      addDefaultMetadataSets();
+	      
+	      // Make sure we always have the extracted metadata set
+	      addRequiredMetadataSets();
+	    }
+	    
+	    collection.cdm = new CollectionDesignManager(new File(getLoadedCollectionCfgFilePath()));
+
+	    // We always set title and description here rather than calling mkcol.pl with Unicode arguments
+
+	    // First though, if we based this collection on another collection and thereyby inherited multilingual
+	    // collection names and descriptions, clear those before setting description and name to new values.
+	    ArrayList colname_metas = collection.cdm.collectionmeta_manager.getMetadata(StaticStrings.COLLECTIONMETADATA_COLLECTIONNAME_STR); // retrieves all languages
+	    ArrayList coldesc_metas = collection.cdm.collectionmeta_manager.getMetadata(StaticStrings.COLLECTIONMETADATA_COLLECTIONEXTRA_STR);
+	    for(int i = 0; i < colname_metas.size(); i++) {
+		CollectionMeta colname_meta = (CollectionMeta)colname_metas.get(i);
+		colname_meta.setValue("");
+	    } 
+	    for(int i = 0; i < coldesc_metas.size(); i++) {
+		CollectionMeta coldesc_meta = (CollectionMeta)coldesc_metas.get(i);
+		coldesc_meta.setValue("");
+	    }
+
+	    // set the collection name and description (in the default language)
+	    CollectionMeta collection_name_collectionmeta = collection.cdm.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_COLLECTIONNAME_STR); // retrieves default language
+	    collection_name_collectionmeta.setValue(title);
+	    CollectionMeta collection_extra_collectionmeta = collection.cdm.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_COLLECTIONEXTRA_STR);
+	    collection_extra_collectionmeta.setValue(description);
+
+	    // Now that we have a CDM, update several settings, such as if we created this collection by basing it on another, set it as public automatically. This update is done to the internal xml structure which may be saved into collect.cfg or collectionConfig.xml accordingly.
+	    if (base_collection_directory != null) {
+		// Update the creator and maintainer
+		CollectionMeta creator_collectionmeta = new CollectionMeta(collection.cdm.collect_config.getCreator());
+		creator_collectionmeta.setValue(email);
+		creator_collectionmeta = null;
+		CollectionMeta maintainer_collectionmeta = new CollectionMeta(collection.cdm.collect_config.getMaintainer());
+		maintainer_collectionmeta.setValue(email);
+		maintainer_collectionmeta = null;
+
+		// All collections based on others are automatically public
+		CollectionMeta public_collectionmeta = new CollectionMeta(collection.cdm.collect_config.getPublic());
+		public_collectionmeta.setValue(StaticStrings.TRUE_STR);
+		public_collectionmeta = null;
+
+		// Finally reset the icons
+		CollectionMeta icon_collection_collectionmeta = collection.cdm.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_ICONCOLLECTION_STR);
+		icon_collection_collectionmeta.setValue(StaticStrings.EMPTY_STR);
+		icon_collection_collectionmeta = null;
+		CollectionMeta icon_collection_small_collectionmeta = collection.cdm.collectionmeta_manager.getMetadatum(StaticStrings.COLLECTIONMETADATA_ICONCOLLECTIONSMALL_STR);
+		icon_collection_small_collectionmeta.setValue(StaticStrings.EMPTY_STR);
+		icon_collection_small_collectionmeta = null;
+	    }
+
+	    saveCollection();
+
+	    // Create a lock file
+	    createLockFile(new File(collection_directory_path, LOCK_FILE));
+
+	    // We're done. Let everyone know.
+	    Gatherer.refresh(Gatherer.COLLECTION_OPENED);
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+    }
+
+    private void scheduling() 
+	throws Exception
+    {
+	//try to obtain email address of collection owner if it exists... 
+	String stmp = Configuration.getEmail(); 
+	if(stmp != null) {
+	    collection.schedule_options.setValue("toaddr", false, Configuration.getEmail());
+	}
+	
+	//The next few items deal with updating the SMTP server, and the to: and from: addresses
+	//from main.cfg and the collection configuration. if no changes are made, or the 
+	//values are result to NULL, any existing values are kept. 
+
+	//try to obtain email address of Greenstone installation webmaster for - used to indicate "sender". 
+	File mcfg = new File(LocalGreenstone.getDirectoryPath() + File.separator + "etc" + File.separator + "main.cfg");
+	BufferedReader maincfg = new BufferedReader(new FileReader(mcfg)); 	    
+	    stmp = "";
+	    String fromaddr = ""; 
+	    while((stmp = maincfg.readLine()) != null) { 
+		if(stmp.startsWith("maintainer")) {
+		       fromaddr = stmp.substring(10); //length of MailServer 
+		   fromaddr = fromaddr.trim();  
+		   break; 
+		}
+	    }
+	    maincfg.close(); 
+	    if(!fromaddr.equals("NULL") && !fromaddr.equals("null")) {
+		collection.schedule_options.setValue("fromaddr", false, fromaddr);	    
+	    }
+
+	    //try to obtain an smtp server address from main.cfg. If that fails,
+	    //try mail.server if an email address exists. If that fails,
+	    //maybe a message to set attribute in main.cfg? 
+	    //i'm pretty sure there exists functionality to do this, but 
+	    //i'll finish this faster if I just wrote it
+
+
+	    maincfg = new BufferedReader(new FileReader(mcfg)); 	  
+	    String smtptmp = "NULL";
+	    while((stmp = maincfg.readLine()) != null) { 
+		if(stmp.startsWith("MailServer")) {
+		       smtptmp = stmp.substring(10); //length of MailServer 
+		   smtptmp = smtptmp.trim(); 
+		   break; 
+		}
+	    }
+            maincfg.close();
+
+            //try if lookup fails
+            if(smtptmp.equals("NULL") || smtptmp.equals("null")) {
+		String email2=fromaddr; 
+		if(!email2.equals("NULL") && !email2.equals("null")) {
+		    int loc = email2.indexOf('@'); 
+		    email2 = email2.substring(loc+1); 
+		    smtptmp = "mail."+email2;
+	        }
+            }
+	    if(!smtptmp.equals("NULL") && !smtptmp.equals("null")) { 
+		collection.schedule_options.setValue("smtp", false, smtptmp);
+	    }
+
+    }
+
+
+    private void createLockFile(File lock_file)
+    {
+	try {
+	    Document default_lockfile = XMLTools.parseXMLFile("xml/" + LOCK_FILE, true);
+	    String user_name = System.getProperty("user.name");
+	    Element person_element = (Element) XMLTools.getNodeFromNamed(default_lockfile.getDocumentElement(), "User");
+	    person_element.appendChild(default_lockfile.createTextNode(user_name));
+	    person_element = null;
+	    user_name = null;
+	    String machine_name = Utility.getMachineName();
+	    Element machine_element = (Element) XMLTools.getNodeFromNamed(default_lockfile.getDocumentElement(), "Machine");
+	    machine_element.appendChild(default_lockfile.createTextNode(machine_name));
+	    machine_element = null;
+	    machine_name = null;
+	    String date_time = Utility.getDateString();
+	    Element date_element = (Element) XMLTools.getNodeFromNamed(default_lockfile.getDocumentElement(), "Date");
+	    date_element.appendChild(default_lockfile.createTextNode(date_time));
+	    date_element = null;
+	    date_time = null;
+	    XMLTools.writeXMLFile(lock_file, default_lockfile);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+
+
+    public boolean deleteCollection(String collection_name)
+    {
+	// First we must release the collection from the local library, if it's running
+	if (LocalLibraryServer.isRunning() == true) {
+	    LocalLibraryServer.releaseCollection(collection_name);
+	}
+
+	// if we're running FLI, it's a fedora collection, and we need to call a special
+	// delete script to delete the collection and all its documents from the fedora
+	// repository and from fedora-gsearch's index.
+	if (Configuration.fedora_info.isActive()) {
+
+	    // Generate the buildcol.pl command
+	    ArrayList command_parts_list = new ArrayList();
+	    if (!Gatherer.isGsdlRemote) {
+		command_parts_list.add(Configuration.perl_path);
+		command_parts_list.add("-S");
+	    }
+
+	    command_parts_list.add(scriptPath + "g2f-deletecol.pl");
+
+	    command_parts_list.add("-hostname");
+	    command_parts_list.add(Configuration.fedora_info.getHostname());
+
+	    command_parts_list.add("-port");
+	    command_parts_list.add(Configuration.fedora_info.getPort());
+
+	    command_parts_list.add("-username");
+	    command_parts_list.add(Configuration.fedora_info.getUsername());
+
+	    command_parts_list.add("-password");
+	    command_parts_list.add(Configuration.fedora_info.getPassword());
+
+	    command_parts_list.add("-protocol");
+	    command_parts_list.add(Configuration.fedora_info.getProtocol());
+
+	    command_parts_list.add("-gli");
+	    command_parts_list.add("-language");
+	    command_parts_list.add(Configuration.getLanguage());
+	    
+	    if(Gatherer.GS3) {
+		command_parts_list.add("-site");
+		command_parts_list.add(Configuration.site_name);
+	    }
+
+	    if(!Gatherer.isGsdlRemote) {
+		command_parts_list.add("-collectdir");
+		command_parts_list.add(getCollectDirectory()); // <../collect/>
+	    }
+	    
+	    command_parts_list.add(collection_name);
+	    
+	    // Run the g2f-deletecol.pl to remove the fedora collection
+	    String[] command_parts = (String[]) command_parts_list.toArray(new String[0]);
+	    GShell shell = new GShell(command_parts, GShell.DELETE, COLLECT, this, null, GShell.GSHELL_FEDORA_COLDELETE); // fedora_coldelete_monitor set to null
+	    shell.start();
+
+	    delete_collection_name = collection_name;
+	    return true; // we assume it succeeded for now, wait until the process returns
+	    // and then proceed to delete the collection directory
+	}
+	else { // not a fedora collection
+
+	    // Delete the collection on the server if we're using a remote Greenstone
+	    if (Gatherer.isGsdlRemote) {
+		Gatherer.remoteGreenstoneServer.deleteCollection(collection_name);
+	    }
+	    
+	    // if Greenstone3, need to deactivate the collection on the server
+	    if (Gatherer.GS3) {
+		Gatherer.configGS3Server(Configuration.site_name, ServletConfiguration.DEACTIVATE_COMMAND + collection_name);
+	    }	    
+	    // Now delete the collection directory
+	    return Utility.delete(new File(getCollectionDirectoryPath(collection_name)));
+	}
+    }
+
+
+    public void fireFileAddedToCollection(File file)
+    {
+	// Send the event off to all the CollectionContentsChangedListeners
+	for (int i = 0; i < collection_contents_changed_listeners.size(); i++) {
+	    ((CollectionContentsChangedListener) collection_contents_changed_listeners.get(i)).fileAddedToCollection(file);
+	}
+    }
+
+
+    /** Retrieve the current collection.
+     * @return The <strong>Collection</strong> itself.
+     */
+    public Collection getCollection() {
+	return collection;
+    }
+
+
+    /** Returns the absolute filename of the specified collection's directory.
+     */
+    static public String getCollectionDirectoryPath(String collection_name)
+    {
+	return Gatherer.getCollectDirectoryPath() + collection_name + File.separator;
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's archives directory.
+     */
+    static public String getLoadedCollectionArchivesDirectoryPath()
+    {
+	return getLoadedCollectionDirectoryPath() + "archives" + File.separator;
+    }
+
+    /** Returns the absolute filename of the loaded collection's export directory.
+     */
+    static public String getLoadedCollectionExportDirectoryPath()
+    {
+	return getLoadedCollectionDirectoryPath() + "export" + File.separator;
+    }
+
+
+
+    /** Returns the absolute filename of the loaded collection's building directory.
+     */
+    static public String getLoadedCollectionBuildingDirectoryPath()
+    {
+	return getLoadedCollectionDirectoryPath() + "building" + File.separator;
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's collect.cfg file.
+     */
+    static public String getLoadedCollectionCfgFilePath()
+    {
+	String path = (Gatherer.GS3 == true)? Utility.COLLECTION_CONFIG_XML : Utility.COLLECT_CFG;
+	return getLoadedCollectionEtcDirectoryPath() + path;
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's directory.
+     */
+    static public String getLoadedCollectionDirectoryPath()
+    {
+	return collection.getCollectionDirectory().getPath() + File.separator;
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's etc directory.
+     */
+    static public String getLoadedCollectionEtcDirectoryPath()
+    {
+	return getLoadedCollectionDirectoryPath() + "etc" + File.separator;
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's .col file.
+     */
+    static public String getLoadedCollectionColFilePath()
+    {
+	return getLoadedCollectionDirectoryPath() + "gli.col";
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's images directory.
+     */
+    static public String getLoadedCollectionImagesDirectoryPath()
+    {
+	return getLoadedCollectionDirectoryPath() + "images" + File.separator;
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's import directory.
+     */
+    static public String getLoadedCollectionImportDirectoryPath()
+    {
+	return getLoadedCollectionDirectoryPath() + "import" + File.separator;
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's index directory.
+     */
+    static public String getLoadedCollectionIndexDirectoryPath()
+    {
+	return getLoadedCollectionDirectoryPath() + "index" + File.separator;
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's log directory.
+     */
+    static public String getLoadedCollectionLogDirectoryPath()
+    {
+	return getLoadedCollectionDirectoryPath() + "log" + File.separator;
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's macros directory.
+     */
+    static public String getLoadedCollectionMacrosDirectoryPath()
+    {
+	return getLoadedCollectionDirectoryPath() + "macros" + File.separator;
+    }
+
+
+    /** Returns the absolute filename of the loaded collection's metadata directory.
+     */
+    static public String getLoadedCollectionMetadataDirectoryPath()
+    {
+	return getLoadedCollectionDirectoryPath() + "metadata" + File.separator;
+    }
+
+
+    /** Returns the (group-qualified) name of the loaded collection with 
+     * OS-dependent file separator.
+     */
+    static public String getLoadedCollectionName()
+    {
+	return CollectionManager.getLoadedCollectionName(false);
+    }
+
+    /** Returns the (group-qualified) name of the loaded collection with 
+     * OS-dependent space separator.
+     * @url true if url-type forward slashes, false if OS-dependent filesystem slashes.
+     */
+    static public String getLoadedCollectionName(boolean url)
+    {
+	if (collection != null) {
+	    //return collection.getName();
+	    return collection.getGroupQualifiedName(url);
+	}
+
+	return null;
+    }
+
+    /** @return the subname of any collection (stripped of any collection-group). */
+    static public String getLoadedCollectionTailName()
+    {
+	if (collection != null) {
+	    return collection.getCollectionTailName();
+	}
+
+	return null;
+    }
+
+    /** Returns the "collectionGroupName/collectionName" or just the collectionName 
+     *  depending on whether the collection is part of a collection group or not.
+     * If url = true, then returns the sub-path as a URL (containing / only), 
+     * and if url = false, then the sub-path is returned in filepath form 
+     * (\ or /, depending on the OS).
+     */
+    static public String getLoadedGroupQualifiedCollectionName(boolean url)
+    {
+	if (collection != null) {
+	    return collection.getGroupQualifiedName(url);
+	}
+
+	return null;
+    }
+
+    public CollectionTree getCollectionTree()
+    {
+	if (collection_tree == null) {
+	    collection_tree = new CollectionTree(collection_tree_model, true);
+	}
+
+	return collection_tree;
+    }
+
+
+    /** Retrieve the tree model associated with the current collection. */
+    public CollectionTreeModel getCollectionTreeModel()
+    {
+	if (collection_tree_model == null && collection != null) {
+	    // Use the import directory to generate a new CollectionTreeModel
+	    collection_tree_model = new CollectionTreeModel(new CollectionTreeNode(new File(getLoadedCollectionImportDirectoryPath())));
+	    // Ensure that the manager is a change listener for the tree.
+	    if (fm_tree_model_listener == null) {
+		fm_tree_model_listener = new FMTreeModelListener();
+	    }
+	    collection_tree_model.addTreeModelListener(fm_tree_model_listener);
+	}
+	return collection_tree_model;
+    }
+
+
+    /** This method when called, creates a new GShell in order to run the import.pl script.
+     * @see org.greenstone.gatherer.Configuration
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.gui.BuildOptions
+     * @see org.greenstone.gatherer.shell.GShell
+     * @see org.greenstone.gatherer.shell.GShellListener
+     * @see org.greenstone.gatherer.shell.GShellProgressMonitor
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    public void importCollection() {
+	importing = true;
+
+	if (!saved()) {
+	    DebugStream.println("CollectionManager.importCollection().forcesave");
+	    import_monitor.saving();
+	    saveCollection();
+	}
+
+	DebugStream.println("CollectionManager.importCollection()");
+	DebugStream.println("Is event dispatch thread: " + SwingUtilities.isEventDispatchThread());
+	//check that we can remove the old index before starting import
+	File index_dir = new File(getLoadedCollectionIndexDirectoryPath());
+	if (index_dir.exists()) {
+	    DebugStream.println("Old Index = " + index_dir.getAbsolutePath()+", testing for deletability");
+	    if (!canDelete(index_dir)) {
+		// tell the user
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Cannot_Delete_Index"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE); 
+		// tell the gui manager
+		// a message for the building log 
+		GShellEvent event = new GShellEvent(this, 0, GShell.IMPORT, Dictionary.get("CollectionManager.Cannot_Delete_Index_Log"), GShell.ERROR);
+		Gatherer.g_man.create_pane.message(event);
+		event = new GShellEvent(this, 0, GShell.IMPORT, "", GShell.ERROR);
+		Gatherer.g_man.create_pane.processComplete(event);
+		importing = false;
+		return;
+	    }
+	}
+
+	// Generate the import.pl command
+	ArrayList command_parts_list = new ArrayList();
+	if (!Gatherer.isGsdlRemote) {
+	    command_parts_list.add(Configuration.perl_path);
+	    command_parts_list.add("-S");
+	}
+
+	if (Configuration.fedora_info.isActive()) {
+	    command_parts_list.add(scriptPath + "g2f-import.pl");
+
+	    command_parts_list.add("-hostname");
+	    command_parts_list.add(Configuration.fedora_info.getHostname());
+
+	    command_parts_list.add("-port");
+	    command_parts_list.add(Configuration.fedora_info.getPort());
+
+	    command_parts_list.add("-username");
+	    command_parts_list.add(Configuration.fedora_info.getUsername());
+
+	    command_parts_list.add("-password");
+	    command_parts_list.add(Configuration.fedora_info.getPassword());
+
+	    command_parts_list.add("-protocol");
+	    command_parts_list.add(Configuration.fedora_info.getProtocol());
+	}
+	else {
+	    String cmdPrefix = null;
+	    if ( CollectionDesignManager.isCompleteBuild() ) {
+		cmdPrefix = "full-";
+		CollectionDesignManager.setImportWasFull( true );
+	    } else {
+		cmdPrefix = "incremental-";
+		CollectionDesignManager.setImportWasFull( false );
+	    }
+	    command_parts_list.add(scriptPath + cmdPrefix + "import.pl"); // scriptPath already set according to local or remote case
+	}
+
+	command_parts_list.add("-gli");
+	command_parts_list.add("-language");
+	command_parts_list.add(Configuration.getLanguage());
+
+	if(Gatherer.GS3) {
+	    command_parts_list.add("-site");
+	    command_parts_list.add(Configuration.site_name);
+	}
+
+	if(!Gatherer.isGsdlRemote) {
+	    command_parts_list.add("-collectdir");
+	    command_parts_list.add(getCollectDirectory());
+	}	
+
+	String[] import_options = collection.import_options.getValues();
+	for (int i = 0; i < import_options.length; i++) {
+        System.err.println( "Tacking on option: " + import_options[i] );
+	    command_parts_list.add(import_options[i]);
+	}
+
+	command_parts_list.add(collection.getGroupQualifiedName(false)); // (colgroup/)colname
+
+	// Run the import.pl command
+	String[] command_parts = (String[]) command_parts_list.toArray(new String[0]);
+	GShell shell = new GShell(command_parts, GShell.IMPORT, BUILDING, this, import_monitor, GShell.GSHELL_IMPORT);
+	//shell.setEventProperty("is_incremental", Boolean.toString(is_incremental));
+	shell.addGShellListener(Gatherer.g_man.create_pane);
+        shell.addGShellListener(Gatherer.g_man.format_pane);
+	shell.start();
+	DebugStream.println("CollectionManager.importCollection().return");
+
+	importing = false;
+    }
+
+
+    public void importMetadataSet(MetadataSet external_metadata_set)
+    {
+	// Copy the .mds file into the collection's "metadata" folder...
+	File external_metadata_set_file = external_metadata_set.getMetadataSetFile();
+
+	// ...but not if it is the redundant "hidden.mds" file
+	if (external_metadata_set_file.getName().equals("hidden.mds")) {
+	    return;
+	}
+
+	// ...and only if it doesn't already exist
+	File metadata_set_file = new File(getLoadedCollectionMetadataDirectoryPath(), external_metadata_set_file.getName());
+	if (!metadata_set_file.exists()) {
+	    try {
+		Gatherer.f_man.getQueue().copyFile(external_metadata_set_file, metadata_set_file, false);
+
+		// If we're using a remote Greenstone server, upload the metadata file
+		if (Gatherer.isGsdlRemote) {
+		    Gatherer.remoteGreenstoneServer.uploadCollectionFile(collection.getGroupQualifiedName(false), metadata_set_file);
+		}
+	    }
+	    catch (Exception exception) {
+		DebugStream.printStackTrace(exception);
+	    }
+
+	    // Load it into the MetadataSetManager
+	    MetadataSetManager.loadMetadataSet(metadata_set_file);
+	}
+    }
+
+
+    /** Determine if we are currently in the middle of importing (and thus, in this case, we can't allow the log writer to exit). Boy was this a mission to track down. The cascade of crap rolls out something like this: Joe Schmo clicks 'Build Collection', which calls the importCollection() method above, which in turn saves the collection with a saveTask, which fires a collectionChanged message once its finished, which drives the list of logs shown on the create pane to update, which fires a itemChanged() event to the OptionsPane who dutifully tells the current log writer thread to finish up writing (all zero lines its been asked to write) and then die. Wereapon Joe Schmo gets a pretty log to look at, but it isn't actually being written to file so the next time he tries to view it faeces hits the air motion cooling device. Joy.
+     * @return true if the gli is currently importing
+     */
+    public boolean isImporting() {
+	return importing;
+    }
+
+
+    public void loadCollection(String collection_file_path)
+    {
+	// Display a modal progress popup to indicate that the collection is being loaded
+	ModalProgressPopup load_collection_progress_popup = new ModalProgressPopup(Dictionary.get("CollectionManager.Loading_Collection"), Dictionary.get("CollectionManager.Loading_Collection_Please_Wait"));
+	load_collection_progress_popup.display();
+
+	// Load the collection on a separate thread so the progress bar updates correctly
+	(new LoadCollectionTask(collection_file_path, load_collection_progress_popup)).start();
+    }
+
+
+    private class LoadCollectionTask
+	extends Thread
+    {
+	private String collection_file_path = null;
+	private ModalProgressPopup load_collection_progress_popup = null;
+
+	public LoadCollectionTask(String collection_file_path, ModalProgressPopup load_collection_progress_popup)
+	{
+	    this.collection_file_path = collection_file_path;
+	    this.load_collection_progress_popup = load_collection_progress_popup;
+	}
+
+	public void run()
+	{
+	    loadCollectionInternal(collection_file_path);
+	    load_collection_progress_popup.close();
+	    Gatherer.setMenuBarEnabled(true);
+	}
+    }
+
+
+    /** Attempts to load the given collection. Currently uses simple serialization of the collection class.
+     * @param location The path to the collection as a <strong>String</strong>.
+     * @see org.greenstone.gatherer.Configuration
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.Collection
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    private void loadCollectionInternal(String location)
+    {
+	DebugStream.println("Loading collection " + location + "...");
+
+
+	// Check we have actually been given a .col file.
+	if (!location.endsWith(".col")) {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Not_Col_File", location), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	    DebugStream.println("CollectionManager.loadCollection: Haven't been given a .col file.");
+	    return;
+	}
+	
+	// Check that the collection configuration file is available
+	File collection_file = new File(location);
+
+	//String collection_name = collection_directory.getName();
+	String collection_name = "";		
+	File collection_directory = collection_file.getParentFile();
+
+	// To get colname = (colgroup/)coltailname, subtract Gatherer.getCollectDirectoryPath() from collection_directory:
+	int index = collection_directory.getAbsolutePath().indexOf(Gatherer.getCollectDirectoryPath());	
+	if(index == -1) {
+	    System.err.println("*** ERROR: collection directory " + collection_directory + " is not located in collect folder: " + Gatherer.getCollectDirectoryPath());
+	} else {
+	    index += Gatherer.getCollectDirectoryPath().length();	    
+	    collection_name = collection_directory.getAbsolutePath().substring(index);
+	}
+
+	if (Gatherer.isGsdlRemote) {
+	    if (Gatherer.remoteGreenstoneServer.downloadCollection(collection_name).equals("")) {
+		return;
+	    }
+	}
+	
+	// Ensure that the collection directory exists
+	if (collection_directory == null || !collection_directory.exists()) {
+	    // We can't open this
+	    System.err.println("CollectionManager.loadCollection: No collection directory.");
+	    return;
+	}
+
+	String file_str = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+	File collection_config_file = new File(collection_directory, file_str);
+	if (!collection_config_file.exists()) {
+	    System.err.println("CollectionManager.loadCollection: No config file.");
+	    collection_directory = null;
+	    collection_config_file = null;
+	    return;
+	}
+
+	// Ensure that an import directory exists for this collection
+	File collection_import_directory = new File(collection_directory, "import");
+	if (!collection_import_directory.exists()) {
+	    collection_import_directory.mkdir();
+	}
+
+	// Special case of a user trying to open an old greenstone collection.
+	boolean non_gli_collection = false;
+	File collection_metadata_directory = new File(collection_directory, "metadata");
+	if (!collection_metadata_directory.exists()) {
+	    DebugStream.println("Loading non-gatherer collection...");
+	    // Show a warning message in case user wants to quit now
+	    non_gli_collection = true;
+	    WarningDialog legacy_dialog = new WarningDialog("warning.LegacyCollection", Dictionary.get("LegacyCollection.Title"), Dictionary.get("LegacyCollection.Message"), null, true);
+	    if (legacy_dialog.display()==JOptionPane.CANCEL_OPTION) {
+		legacy_dialog.dispose();
+		collection_directory = null;
+		collection_config_file = null;
+		return;
+	    }
+	    legacy_dialog.dispose();
+
+	}
+
+	// Now determine if a lock already exists on this collection.
+	File lock_file = new File(collection_file.getParentFile(), LOCK_FILE);
+	if (lock_file.exists()) {
+	    LockFileDialog dialog = new LockFileDialog(Gatherer.g_man, collection_name, lock_file);
+	    int choice = dialog.getChoice();
+	    dialog.dispose();
+	    dialog = null;
+
+	    if (choice != LockFileDialog.YES_OPTION) {
+		// user has cancelled
+		lock_file = null;
+		collection_directory = null;
+		collection_config_file = null;
+		return;
+	    }
+
+	    lock_file.delete();
+	}
+
+	// now we are using gli.col - old colls may have used the collection name
+	if (!collection_file.exists()) {
+	  File old_coll_file = new File(collection_directory, collection_name+".col");
+	  if (old_coll_file.exists()) {
+	    try {
+	      old_coll_file.renameTo(collection_file);
+	    } catch (Exception e) {
+	      DebugStream.println("Couldn't rename "+old_coll_file.getName()+" to gli.col. Will just carry on with default gli.col");
+	      // but just carry on.
+	    }
+	  }
+	}
+
+	try {
+	    // Create a lock file.
+	    createLockFile(lock_file);
+	    // This lock file may not have been created so check
+	    if(!lock_file.canWrite()) {
+		// The lock file cannot be written to. Most likely cause incorrect file permissions.
+		System.err.println("Cannot write lock file!");
+		String args[] = new String[2];
+		args[0] = location;		
+		args[1] = Dictionary.get("FileActions.Write_Not_Permitted_Message", new String[]{lock_file.getAbsolutePath()});		
+		if(Gatherer.client_operating_system.toUpperCase().indexOf("WINDOWS")!=-1){		  
+		  //if(Gatherer.client_operating_system.toUpperCase().indexOf("VISTA")!=-1){		    
+		    args[1] += Dictionary.get("FileActions.File_Permission_Detail", new String[]{Configuration.gsdl_path, System.getProperty("user.name")});
+		  //} 
+		}
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Cannot_Open_With_Reason", args), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		args = null;
+		return;
+	    }
+	    
+	    // need to fix this up as currently it craps out if the .col file is not there, which is may not always be.
+	    if (canDoScheduling() && collection_file.exists()) {
+	      //THIS LOOKS LIKE THE BEST PLACE TO TRY AND UPDATE .col FILES FOR EXISTING COLLECTIONS...Wendy
+	      // Don't need to update anything if collection_file doesn't exist yet.
+		//First, see if "Schedule" exists in the XMl File... 
+		BufferedReader bir = new BufferedReader(new FileReader(collection_file)); 
+		boolean flag = false; 
+		try {
+		    String stmp = new String(); 
+	       
+		    while((stmp = bir.readLine()) != null) {
+			stmp = stmp.trim();  
+			if(stmp.equals("<Schedule>") || stmp.equals("<Schedule/>")) {
+			    flag = true; 
+			    break; 
+			}
+		    }
+		    bir.close(); 
+		    
+		} catch (IOException ioe) {
+		    DebugStream.printStackTrace(ioe);
+		}
+		
+		//modify if old .col (i.e. no Schedule exists in XML file)
+		if(!flag) {
+		    File new_collection_file = new File(collection_directory.getAbsolutePath() + "/tmp.col");
+		    
+		    
+		    BufferedWriter bor = new BufferedWriter(new FileWriter(new_collection_file)); 
+		    bir = new BufferedReader(new FileReader(collection_file)); 
+		    
+		    try { 
+			String stmp = new String(); 
+			while((stmp = bir.readLine()) != null) {
+			    String stmp2 = stmp.trim(); 
+			    if(stmp2.startsWith("<!ELEMENT Argument")) {
+				bor.write("  <!ELEMENT Schedule          (Arguments*)>\n"); 			   
+			    }
+			    else if(stmp2.equals("</BuildConfig>")) {
+				bor.write("      <Schedule/>\n"); 
+			    }
+			    
+			    bor.write(stmp + "\n"); 
+			    
+			}
+			bir.close(); 
+			bor.close();
+		    } catch (IOException ioe) {  
+			DebugStream.printStackTrace(ioe);
+		    }  
+		    
+		    //copy over tmp.col to replace 
+		    try { 
+			collection_file.delete();
+			new_collection_file.renameTo(collection_file); 
+		    } catch (Exception e) { 
+			DebugStream.printStackTrace(e);
+		    } 
+		}
+	    }
+	    
+	    // Open the collection file
+	    this.collection = new Collection(collection_file);
+	    if (collection.error) {
+		collection = null;
+		// Remove lock file
+		if (lock_file.exists()) {
+		    lock_file.delete();
+		}
+		throw(new Exception(Dictionary.get("CollectionManager.Missing_Config"))); // this error message does not agree with the error
+	    }
+	    
+	    if (canDoScheduling()) {
+		scheduling();
+	    }
+	  
+	    // These may have been set in the past, but are no longer used 
+	    // by GLI
+	    collection.import_options.removeValue("removeold");
+	    collection.import_options.removeValue("keepold");
+
+            MetadataSetManager.clearMetadataSets();
+	    MetadataSetManager.loadMetadataSets(collection_metadata_directory);
+
+	    // Make sure we always have the extracted metadata set
+	    addRequiredMetadataSets();
+
+	    ProfileXMLFileManager.loadProfileXMLFile(collection_metadata_directory);
+ 
+	    // If this is a non-GLI (legacy) collection, load the default metadata sets
+	    if (non_gli_collection) {
+		addDefaultMetadataSets();
+
+		// Recurse the import folder tree, backing up the metadata.xml files before they are edited
+		LegacyCollectionImporter.backupMetadataXMLFiles(collection_directory);
+	    }
+
+	    // Read through the metadata.xml files in the import directory, building up the metadata value trees
+	    MetadataXMLFileManager.clearMetadataXMLFiles();
+	    MetadataXMLFileManager.loadMetadataXMLFiles(collection_import_directory,collection.toSkimFile());
+
+	    
+	    // get rid of the previous scan through docxml files
+	    DocXMLFileManager.clearDocXMLFiles();
+
+	    if (Configuration.fedora_info.isActive()) { // FLI case
+		// Read through the docmets.xml files in the export directory
+		File collection_export_directory = new File(getLoadedCollectionExportDirectoryPath());
+		DocXMLFileManager.loadDocXMLFiles(collection_export_directory,"docmets.xml");
+	    }
+	    else {
+		// Read through the doc.xml files in the archives directory
+		File collection_archives_directory = new File(getLoadedCollectionArchivesDirectoryPath());
+		DocXMLFileManager.loadDocXMLFiles(collection_archives_directory,"doc.xml");
+	    }
+
+
+	    // Get a list of the collection specific classifiers and plugins
+	    Classifiers.loadClassifiersList(collection_name);
+	    Plugins.loadPluginsList(collection_name);
+
+	    collection.cdm = new CollectionDesignManager(collection_config_file);
+	    if (non_gli_collection) {
+		// Change the classifiers to use the namespaced element names
+		LegacyCollectionImporter.updateClassifiers(collection.cdm);
+	    }
+
+	    // We're done. Let everyone know.
+	    DebugStream.println(Dictionary.get("CollectionManager.Loading_Successful", collection_name));
+	    Gatherer.refresh(Gatherer.COLLECTION_OPENED);
+	}
+	catch (Exception error) {
+	    // There is obviously no existing collection present.
+	    DebugStream.printStackTrace(error);
+	    error.printStackTrace();
+	    if(error.getMessage() != null) {
+		String[] args = new String[2];
+		args[0] = location;		
+		args[1] = error.getMessage();
+		//args[1] = "The Librarian Interface does not have permission to write to... Please check file permissions.";
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Cannot_Open_With_Reason", args), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	    }
+	    else {
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Cannot_Open", location), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	    }
+	}
+
+	lock_file = null;
+	collection_directory = null;
+	collection_config_file = null;
+    }
+
+    /** At present, scheduling only works for GS2, only when GS2 is local and only when GLI runs from
+     * within a GS2 installation. This method can be adjusted as scheduling becomes available for more
+     * more situations. */
+    public static boolean canDoScheduling() {
+	// Would be nice to support more of these, rather than returning false
+	if(Gatherer.isGsdlRemote) {
+	    return false;
+	} 
+	if(Gatherer.GS3) {
+	    return false;
+	}
+	if (Configuration.fedora_info.isActive()) {
+	    return false;
+	}
+
+	// GS2's etc/main.cfg is necessary for scheduling, but scheduling looks for it locally: 
+	// it assumes GLI is inside a GS2 installation
+	File mcfg = new File(LocalGreenstone.getDirectoryPath() + File.separator + "etc" + File.separator + "main.cfg");
+	if(!mcfg.exists()) {
+	    System.out.println("Cannot do scheduling, since there is no file: " + mcfg.getAbsolutePath() 
+			       + ".\nScheduling presently depends on GLI running from inside a GS2.");
+	    return false;
+	}
+	
+	return true;
+    }
+
+    private void makeCollection(String name, String email)
+    {
+	// Generate the mkcol.pl command
+	ArrayList command_parts_list = new ArrayList();
+	if (!Gatherer.isGsdlRemote) {
+	    command_parts_list.add(Configuration.perl_path);
+	    command_parts_list.add("-S");
+	}
+	command_parts_list.add(scriptPath + "mkcol.pl");
+	if(Gatherer.GS3) {
+	    command_parts_list.add(Utility.GS3MODE_ARGUMENT); // add '-gs3mode' 
+	    command_parts_list.add("-site");
+	    command_parts_list.add(Configuration.site_name);
+	}
+
+	if(!Gatherer.isGsdlRemote) {
+	    command_parts_list.add("-collectdir");
+	    command_parts_list.add(getDefaultCollectDirectory());
+ 	}
+	command_parts_list.add("-win31compat");
+	command_parts_list.add((Gatherer.isGsdlRemote) ? "false" : "true");
+
+	if (email != null && !email.equals("")) {
+	    command_parts_list.add("-creator");
+	    command_parts_list.add(email);
+	}
+
+	command_parts_list.add(name);
+	
+	// Run the mkcol.pl command
+	String[] command_parts = (String[]) command_parts_list.toArray(new String[0]);
+	//for(int i = 0; i < command_parts.length; i++) {
+	///ystem.err.println("\""+command_parts[i]+"\"");
+	//}
+	
+	GShell process = new GShell(command_parts, GShell.NEW, COLLECT, this, null, GShell.GSHELL_NEW);
+	process.run(); // Don't bother threading this... yet
+    }
+
+
+    /** Any implementation of GShellListener must include this method to allow the GShell to send messages to listeners. However in this case the CollectionManager is in no way interested in what the messages are, just the import events which have a specific type and are handled elsewhere. Thus we can safely ignore this event.
+     * @param event A <strong>GShellEvent</strong> which contains a the message.
+     */
+    public synchronized void message(GShellEvent event) {
+      
+    }
+
+
+    public void metadataChanged(CollectionTreeNode[] file_nodes)
+    {
+	if (collection != null) {
+	    collection.setMetadataChanged(true);
+	    
+		// we're only going to refresh the tree's already visible nodes (and reselect them) IFF 
+		// gs.FilenameEncoding meta was set on any files/folders and the filenames of the
+		// affected CollectionTreeNodes need to be recalculated
+	    if(FilenameEncoding.isRefreshRequired()) {
+			
+			// refreshes the relevant part of the tree
+			TreePath[] paths = collection_tree.getSelectionPaths();	       
+			if(paths != null) {
+			    for(int i = 0; i < paths.length; i++) {
+				collection_tree_model.refresh(paths[i]);
+				collection_tree.setSelectionPath(paths[i]);
+			    }
+			}
+			// refreshed the tree, so turn off the requirement to refresh
+			FilenameEncoding.setRefreshRequired(false);		
+	    }
+	}
+    }
+
+
+    public void openCollectionFromLastTime() {
+      // If there was an open collection last session, reopen it. 
+	  // (this method doesn't get called if there was no open collection or only a collect folder 
+	  // instead of any previously-opened collection)
+	  
+		// Load the collection now
+		loadCollection(Gatherer.open_collection_file_path);      
+    }
+
+
+    /** This call is fired whenever a process within a GShell created by this class begins.
+     * @param event A <strong>GShellEvent</strong> containing information about the GShell process.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.gui.GUIManager
+     * @see org.greenstone.gatherer.shell.GShell
+     */
+    public synchronized void processBegun(GShellEvent event) {
+	DebugStream.println("CollectionManager.processBegun(" + event.getType() + ")");
+	///ystem.err.println("ProcessBegun " + event.getType());
+	// If this is one of the types where we wish to lock user control
+	Gatherer.g_man.lockCollection((event.getType() == GShell.IMPORT), true);
+    }
+    /** This call is fired whenever a process within a GShell created by this class ends.
+     * @param event A <strong>GShellEvent</strong> containing information about the GShell process.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.gui.GUIManager
+     * @see org.greenstone.gatherer.shell.GShell
+     */
+    public synchronized void processComplete(GShellEvent event) {
+	//ystem.err.println("CollectionManager.processComplete(" + event.getType() + ")");
+	Gatherer.g_man.lockCollection((event.getType() == GShell.IMPORT), false);
+	///ystem.err.println("Received process complete event - " + event);
+	// If we were running an import, now run a build.
+	if(event.getType() == GShell.IMPORT && event.getStatus() == GShell.OK) {
+	    // Finish import.
+	    collection.setImported(true);
+	    collection.setFilesChanged(false);
+	    collection.setMetadataChanged(false);
+	    buildCollection();
+	}
+	else if(event.getType() == GShell.SCHEDULE && event.getStatus() == GShell.OK ) {
+
+	   	WarningDialog collection_built_warning_dialog = new WarningDialog("warning.ScheduleBuilt", Dictionary.get("ScheduleBuilt.Title"), Dictionary.get("ScheduleBuilt.Message"), null, false);
+  		collection_built_warning_dialog.setMessageOnly(true); // Not a warning
+  		collection_built_warning_dialog.display();
+  		collection_built_warning_dialog.dispose();
+  		collection_built_warning_dialog = null;   
+	} 
+	// If we were running a build, now is when we move files across.
+	else if(event.getType() == GShell.BUILD && event.getStatus() == GShell.OK) {
+            
+        if ( CollectionDesignManager.buildcolWasFull() ) {
+
+	    // No installCollection() for GS3 solr collection: activate will take care of that
+	    // and no call to configGS3Server for solr collection
+	    if(CollectionManager.isSolrCollection()) {
+		
+		DebugStream.println("Solr collection build complete: building already moved to index by activate.pl.");
+		
+		// Finished building,
+		// For now, for a GS3 solr collection, we'd have stopped the GS3 server before building
+		// and will now need to restart it.
+		/*GS3ServerThread thread = new GS3ServerThread(Configuration.gsdl3_src_path, "restart");
+		  thread.start();*/
+		
+		// Give the GS3 server time to restart:
+		// the GS3ServerThread above waits for the process to terminate. ant restart target calls the start-tomcat ant target
+		// and that takes waits 5 seconds and polls to see if a GS3 server index page has loaded. So no need to sleep here		
+		
+	    }
+	    
+	    else if(installCollection()) {
+		    // If we have a local library running then ask it to add our newly created collection
+		    if (LocalLibraryServer.isRunning() == true) {
+		        LocalLibraryServer.addCollection(collection.getName());
+		    }
+		    else if (Gatherer.GS3) {
+		        //xiao comment out this: convertToGS3Collection();
+		        Gatherer.configGS3Server(Configuration.site_name, ServletConfiguration.ADD_COMMAND + collection.getName());
+		    }  
+
+		    // Fire a collection changed first to update the preview etc buttons
+		    Gatherer.refresh(Gatherer.COLLECTION_REBUILT);
+
+      		// Now display a message dialog saying its all built
+      		WarningDialog collection_built_warning_dialog = new WarningDialog("warning.CollectionBuilt", Dictionary.get("CollectionBuilt.Title"), Dictionary.get("CollectionBuilt.Message"), null, false);
+      		collection_built_warning_dialog.setMessageOnly(true); // Not a warning
+      		collection_built_warning_dialog.display();
+      		collection_built_warning_dialog.dispose();
+      		collection_built_warning_dialog = null;
+
+		    //Set nothing as needing rebuilding, as a build has just finished :-)
+		    CollectionDesignManager.resetRebuildTypeRequired();
+	        }
+	        else {
+		    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Preview_Ready_Failed"), Dictionary.get("CollectionManager.Preview_Ready_Title"), JOptionPane.ERROR_MESSAGE);
+		    Gatherer.refresh(Gatherer.COLLECTION_REBUILT);
+		    DebugStream.println("Status is ok but !installCollection()");
+	        }
+        }
+	}
+
+	else if(event.getType() == GShell.DELETE) {
+
+	    // we can only get here if we tried to delete a fedora collection
+
+	    if(event.getStatus() == GShell.ERROR) { // error purging the collection from fedora
+		
+		JOptionPane.showMessageDialog(Gatherer.g_man,Dictionary.get("DeleteCollectionPrompt.Failed_Fedora_Delete", new String[]{delete_collection_name}),Dictionary.get("DeleteCollectionPrompt.Failed_Title"),JOptionPane.WARNING_MESSAGE);
+		delete_collection_name = null; // re-zero
+	    }
+	    
+	    else if(event.getStatus() == GShell.OK) { // fedora purge was successful
+
+		if(delete_collection_name != null) {
+		    if (Gatherer.isGsdlRemote) {
+			Gatherer.remoteGreenstoneServer.deleteCollection(delete_collection_name);
+		    }
+		    
+		    // if Greenstone3, need to deactivate the collection on the server
+		    if (Gatherer.GS3) {
+			Gatherer.configGS3Server(Configuration.site_name, ServletConfiguration.DEACTIVATE_COMMAND + delete_collection_name);
+		    }
+
+		    // Now at last, can delete the collection directory as for a normal collection
+		    boolean success = Utility.delete(new File(getCollectionDirectoryPath(delete_collection_name)));
+
+		    if (!success) {
+			JOptionPane.showMessageDialog(Gatherer.g_man,Dictionary.get("DeleteCollectionPrompt.Failed_Delete", new String[]{delete_collection_name}),Dictionary.get("DeleteCollectionPrompt.Failed_Title"),JOptionPane.WARNING_MESSAGE);
+		    }
+		    delete_collection_name = null; // re-zero
+		}
+
+	    }
+	}
+
+	else if (event.getStatus() == GShell.CANCELLED) {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Build_Cancelled"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	    Gatherer.g_man.repaint();
+	}
+	else if (event.getStatus() == GShell.ERROR) {	    
+	    if (event.getType() == GShell.NEW) {	 
+	      String name = event.getMessage();
+	      String collectDir = getCollectionDirectoryPath(name);
+	      String errMsg = "";
+	      if (!new File(getCollectionDirectoryPath(name)).exists() || !new File(getCollectionDirectoryPath(name)).canWrite()) {	      
+		String reason = Dictionary.get("FileActions.Write_Not_Permitted_Message", new String[]{collectDir});
+		errMsg = Dictionary.get("CollectionManager.Cannot_Create_Collection_With_Reason", new String[]{reason});
+		if(Gatherer.client_operating_system.toUpperCase().indexOf("WINDOWS") != -1){		  
+		  //if(Gatherer.client_operating_system.toUpperCase().indexOf("VISTA")!=-1){		    
+		    errMsg += Dictionary.get("FileActions.File_Permission_Detail", new String[]{Configuration.gsdl_path, System.getProperty("user.name")});
+		  //} 
+		}
+	      } else {
+		errMsg = Dictionary.get("CollectionManager.Cannot_Create_Collection");
+	      }
+	      JOptionPane.showMessageDialog(Gatherer.g_man, errMsg, Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	    } 
+	    else if(event.getType() == GShell.SCHEDULE) { 
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Schedule_Failed"), Dictionary.get("CollectionManager.Schedule_Ready_Title"), JOptionPane.ERROR_MESSAGE);
+	    }
+	    else {
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Preview_Ready_Failed"), Dictionary.get("CollectionManager.Preview_Ready_Title"), JOptionPane.ERROR_MESSAGE);
+		Gatherer.refresh(Gatherer.COLLECTION_REBUILT);
+	    }
+	    
+	    Gatherer.g_man.repaint(); // It appears Java's own dialogs have the same not always painting correct area bug that I suffer from. Well I don't suffer from it personally, but my ModalDialog components do.
+	}
+    }
+
+
+    /** Determine if the manager is ready for actions apon its collection.
+     * @return A <i>boolean</i> which is <i>true</i> to indicate a collection has been loaded and thus the collection is ready for editing, <i>false</i> otherwise.
+     */
+    static public synchronized boolean ready() {
+	if(collection != null) {
+	    return true;
+	}
+	else {
+	    return false;
+	}
+    }
+
+
+    /** This method associates the collection build monitor with the build monitor created in CreatePane.
+     * @param monitor A <strong>GShellProgressMonitor</strong> which we will use as the build monitor.
+     */
+    public void registerBuildMonitor(GShellProgressMonitor monitor) {
+	build_monitor = monitor;
+    }
+    /** This method associates the collection import monitor with the import monitor created in CreatePane.
+     * @param monitor A <strong>GShellProgressMonitor</strong> which we will use as the import monitor.
+     */
+    public void registerImportMonitor(GShellProgressMonitor monitor) {
+	import_monitor = monitor;
+    }
+
+    public void registerScheduleMonitor(GShellProgressMonitor monitor) { 
+	schedule_monitor = monitor; 
+    }
+
+
+    static public void removeCollectionContentsChangedListener(CollectionContentsChangedListener listener)
+    {
+	collection_contents_changed_listeners.remove(listener);
+    }
+
+
+    public void removeMetadataSet(MetadataSet metadata_set)
+    {
+	DebugStream.println("Removing metadata set...");
+
+	// Delete the .mds file from the collection's "metadata" folder...
+	File metadata_set_file = metadata_set.getMetadataSetFile();
+
+	// ...but not if it is the "ex.mds" file
+	if (metadata_set_file.getName().equals("ex.mds")) {
+	    return;
+	}
+
+	// ...and only if it exists
+	if (metadata_set_file.exists()) {
+	    metadata_set_file.delete();
+
+	    // Unload it from the MetadataSetManager
+	    MetadataSetManager.unloadMetadataSet(metadata_set);
+
+	    // If we're using a remote Greenstone server, delete the metadata file on the server
+	    if (Gatherer.isGsdlRemote) {
+		Gatherer.remoteGreenstoneServer.deleteCollectionFile(collection.getGroupQualifiedName(false), metadata_set_file);
+	    }
+	}
+    }
+
+
+    /** Used to check whether all open collections have a 'saved' state.
+     * @return A <i>boolean</i> which is <i>true</i> if the collection has been saved.
+     * @see org.greenstone.gatherer.collection.Collection
+     */
+    public boolean saved() {
+	boolean result = true;
+	if(collection != null) {
+	    result = collection.getSaved();
+	}
+	return result;
+    }
+
+
+    /** Saves the currently loaded collection. */
+    public void saveCollection()
+    {
+
+        if (collection == null) return;
+
+	DebugStream.println("Saving collection " + collection.getName() + "...");
+
+	// Change cursor to hourglass
+	Gatherer.g_man.wait(true);
+
+	// Create a backup of the collection file, just in case anything goes wrong
+	File collection_file = new File(getLoadedCollectionColFilePath());
+	if (collection_file.exists()) {
+	    File collection_file_backup = new File(collection_file.getAbsolutePath() + "~");
+	    if (!collection_file.renameTo(collection_file_backup)) {
+		DebugStream.println("Error in CollectionManager.saveCollection(): could not create backup file.");
+	    }
+	    collection_file_backup.deleteOnExit();
+	}
+
+	// Write out the collection file
+	collection.save();
+
+	// Write out the collection configuration file
+	collection.cdm.save();
+
+	// Change cursor back to normal
+	Gatherer.g_man.wait(false);
+    }
+
+
+    /** I started giving the user the choice of using an existing meta set or creating a new one. The second option being so that they didn't have to add/merge/ignore each element, they could all be added automatically. However, I am not sure where the merge prompt gets called from, and it is not essential, so I am leaving it for now - it should be added back in and finished. [kjdon] */
+  // now add in greenstone metadata set too.
+    private void addDefaultMetadataSets()
+    {
+	// Add dublin core which is the default metadata set. The user
+	// can change this later
+	File dc_file = new File(Gatherer.getGLIMetadataDirectoryPath()+"dublin.mds");
+	if (dc_file.exists()) {
+	    importMetadataSet(new MetadataSet(dc_file));
+	}
+	File gs_file = new File(Gatherer.getGLIMetadataDirectoryPath()+"greenstone.mds");
+	if (gs_file.exists()) {
+	    importMetadataSet(new MetadataSet(gs_file));
+	}
+    }
+
+
+    private void addRequiredMetadataSets()
+    {
+	// Always import the extracted metadata set
+	File extracted_metadata_set_file = new File(Gatherer.getGLIMetadataDirectoryPath() + MetadataSetManager.EXTRACTED_METADATA_NAMESPACE + StaticStrings.METADATA_SET_EXTENSION);
+	importMetadataSet(new MetadataSet(extracted_metadata_set_file));
+    }
+
+    private String getDefaultCollectDirectory() {
+	String collect_dir = Gatherer.getCollectDirectoryPath();
+	// Remove erroneous file windows file separator as it causes problems when running import.pl
+	if(collect_dir.length() > 2 && collect_dir.endsWith("\\")) {
+	    collect_dir = collect_dir.substring(0, collect_dir.length() - 1);
+	}
+	return collect_dir;
+    }
+
+    // used as arg in the perl scripts
+    private String getCollectDirectory() {
+	String collect_dir = Gatherer.getCollectDirectoryPath();	
+	return collect_dir.substring(0, collect_dir.length()-1); // remove trailing slash
+	
+	// the following will stick any colgroup at the end of the collect directory, making it no longer
+	// possible to get the real collect dir in a general manner if this were located outside greenstone
+	//String collect_dir = collection.getCollectionDirectory().getParentFile().getPath();
+	//return collect_dir;
+    }
+
+    public static String getBuildType() {
+	String buildType = (new CollectionMeta( CollectionDesignManager.collect_config.getBuildType() )).getValue(CollectionMeta.TEXT);
+	return buildType;
+    }
+
+    public static boolean isSolrCollection() {
+	return (Gatherer.GS3 && CollectionManager.getBuildType().equals("solr"));
+    }
+
+
+    /** Install collection by moving its files from building to index after a successful build.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    private boolean installCollection()
+    {
+	if (Configuration.fedora_info.isActive()) {
+	    DebugStream.println("Fedora build complete. No need to move files.");
+	    return true;
+	}
+
+	DebugStream.println("Build complete. Moving files.");
+
+	try {
+	    // Ensure that the local library has released this collection so we can delete the index directory
+	    if (LocalLibraryServer.isRunning() == true) {
+		LocalLibraryServer.releaseCollection(getLoadedCollectionName(true)); // URL style slash //collection.getName());
+	    }
+	    // deactivate it in tomcat so that windows will release the index files
+	    if (Gatherer.GS3 && !Gatherer.isGsdlRemote) {
+		Gatherer.configGS3Server(Configuration.site_name, ServletConfiguration.DEACTIVATE_COMMAND + collection.getName());
+	    }
+	    /*
+	    File index_dir = new File(getLoadedCollectionIndexDirectoryPath());
+	    DebugStream.println("Index = " + index_dir.getAbsolutePath());
+
+	    File building_dir = new File(getLoadedCollectionBuildingDirectoryPath());
+	    DebugStream.println("Building = " + building_dir.getAbsolutePath());
+
+	    // Get the build mode from the build options
+	    String build_mode = collection.build_options.getValue("mode");
+
+	    // Special case for build mode "all": replace index dir with building dir
+	    if (build_mode == null || build_mode.equals(Dictionary.get("CreatePane.Mode_All"))) {
+		// Remove the old index directory
+		if (index_dir.exists()) {
+		    Utility.delete(index_dir);
+
+		    // Wait for a couple of seconds, just for luck
+		    wait(2000);
+
+		    // Check the delete worked
+		    if (index_dir.exists()) {
+			throw new Exception(Dictionary.get("CollectionManager.Index_Not_Deleted"));
+		    }
+		}
+
+		if (Gatherer.isGsdlRemote) {
+		    Gatherer.remoteGreenstoneServer.deleteCollectionFile(
+                             collection.getGroupQualifiedName(false), new File(getLoadedCollectionIndexDirectoryPath()));
+		    Gatherer.remoteGreenstoneServer.moveCollectionFile(collection.getGroupQualifiedName(false), 
+                    new File(getLoadedCollectionBuildingDirectoryPath()), new File(getLoadedCollectionIndexDirectoryPath()));
+		}
+
+		// Move the building directory to become the new index directory
+		if (building_dir.renameTo(index_dir) == false) {
+		    throw new Exception(Dictionary.get("CollectionManager.Build_Not_Moved"));
+		}
+	    }
+
+	    // Otherwise copy everything in the building dir into the index dir
+	    else {
+		moveContentsInto(building_dir, index_dir);
+	    }
+
+	    // move oai tmpdb to oai livedb
+	    */
+	    
+	}
+	catch (Exception exception) {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CollectionManager.Install_Exception", exception.getMessage()), "Error", JOptionPane.ERROR_MESSAGE);
+	    return false;
+	}
+	return true;
+    }
+
+
+    /** Moves all the files in one directory into another, overwriting existing files */
+    private void moveContentsInto(File source_directory, File target_directory)
+    {
+	File[] source_files = source_directory.listFiles();
+	for (int i = 0; i < source_files.length; i++) {
+	    File source_file = source_files[i];
+	    File target_file = new File(target_directory, source_file.getName());
+
+	    if (source_file.isDirectory()) {
+		moveContentsInto(source_file, target_file);
+		source_file.delete();
+	    }
+	    else {
+		if (target_file.exists()) {
+		    target_file.delete();
+		}
+
+		source_file.renameTo(target_file);
+	    }
+	}
+    }
+
+    private void updateCollectionConfigXML(File base_cfg, File new_cfg) {
+	//In this method, the files base_cfg and new_cfg are all xml files.
+
+	Document base_cfg_doc = XMLTools.parseXMLFile(base_cfg);
+	XMLTools.writeXMLFile(new_cfg, base_cfg_doc);
+	Document new_cfg_doc = XMLTools.parseXMLFile(new_cfg);
+	Element collection_config = new_cfg_doc.getDocumentElement();
+	
+	Node browseNode = XMLTools.getChildByTagNameIndexed(collection_config, StaticStrings.BROWSE_STR, 0);
+	NodeList classifier_children = ((Element)browseNode).getElementsByTagName(StaticStrings.CLASSIFIER_STR);
+	int num_nodes = classifier_children.getLength();
+	
+	if (num_nodes < 1) {
+	    return;
+	}
+
+		// Read in the classifier command watching for hfile, metadata and sort arguments.
+		String buttonname = null;
+		String hfile = null;
+		String metadata = null;
+		String sort = null;
+		
+	for (int i=0; i<num_nodes; i++) {
+	    Element classifier_element = (Element)classifier_children.item(i);
+	    NodeList option_children = classifier_element.getElementsByTagName(StaticStrings.OPTION_STR);
+	    for (int j=0; j<option_children.getLength(); j++) {
+		Element option_element = (Element)option_children.item(j);
+	        String name_str = option_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+ 		String value_str = option_element.getAttribute(StaticStrings.VALUE_ATTRIBUTE);
+		    
+		if (name_str == null || name_str.equals("")) {
+		    continue;
+		}
+		if (name_str != null && value_str == null ) {
+		    value_str = "";
+		}
+		if (name_str.equals("hfile")) {
+		    hfile = value_str;
+		} 
+		else if (name_str.equals("metadata") && value_str != null) { 
+		    String replacement = ProfileXMLFileManager.getMetadataElementFor(value_str);
+		    if (replacement != null && !replacement.equals("")) {
+			metadata = replacement;
+		    }
+		}
+		else if (name_str.equals("sort") && value_str != null) { 
+		    String replacement = ProfileXMLFileManager.getMetadataElementFor(value_str);
+		    if (replacement != null && !replacement.equals("")) {
+			sort = replacement;
+		    }
+		}
+		else if(name_str.equals("buttonname") && value_str != null) {
+		    buttonname = value_str;
+		}
+	    }
+	}
+	for (int i=0; i<num_nodes; i++) {
+	    Element classifier_element = (Element)classifier_children.item(i);
+	    NodeList option_children = classifier_element.getElementsByTagName(StaticStrings.OPTION_STR);
+	    for (int j=0; j<option_children.getLength(); j++) {
+		Element option_element = (Element)option_children.item(j);
+	        String name_str = option_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+		
+		if (name_str.equals("metadata") && metadata != null) { 
+		    option_element.setAttribute(StaticStrings.VALUE_ATTRIBUTE, metadata);
+		}
+		else if (name_str.equals("hfile") && hfile != null) {
+		    option_element.setAttribute(StaticStrings.VALUE_ATTRIBUTE, metadata + ".txt");
+		}
+		else if (name_str.equals("sort") && sort != null) { 
+		    option_element.setAttribute(StaticStrings.VALUE_ATTRIBUTE, sort);
+		}
+		else if(name_str.equals("buttonname") && (buttonname == "" || buttonname == null)) {
+		    // No buttonname has been specified. Lets create one using the metadata as its value
+		    Element option = new_cfg_doc.createElement(StaticStrings.OPTION_STR);
+		    option.setAttribute(StaticStrings.NAME_ATTRIBUTE, "buttonname");
+		    option_element.setAttribute(StaticStrings.VALUE_ATTRIBUTE, metadata);	
+		    classifier_element.appendChild(option);
+		}
+	    }
+	}
+    }
+	
+    private void updateCollectionCFG(File base_cfg, File new_cfg, String description, String email, String title)
+    {
+	boolean first_name = true;
+	boolean first_extra = true;
+	
+	// Now read in base_cfg line by line, parsing important onces and/or replacing them with information pertinent to our collection. Each line is then written back out to the new collect.cfg file.
+	try {
+	    BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(base_cfg), "UTF-8"));
+	    BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new_cfg), "UTF-8"));
+	    String command = null;
+	    while((command = in.readLine()) != null) {
+		if (command.length()==0) {
+		    // output a new line
+		    out.newLine();
+		    continue;
+		}
+		// We have to test the end of command for the special character '\'. If found, remove it and append the next line, then repeat.
+		while(command.trim().endsWith("\\")) {
+		    command = command.substring(0, command.lastIndexOf("\\"));
+		    String next_line = in.readLine();
+		    if(next_line != null) {
+			command = command + next_line;
+		    }
+		}
+		// commands can extend over more than one line so use the CommandTokenizer which takes care of that
+		CommandTokenizer tokenizer = new CommandTokenizer(command, in, false);
+		String command_type_str = tokenizer.nextToken().toLowerCase();
+
+		if (command_type_str.equals(StaticStrings.COLLECTIONMETADATA_STR)) {
+		    // read the whole thing in, but for collectionname, collectionextra, iconcollection, iconcollectionsmall we will ignore them
+		    StringBuffer new_command = new StringBuffer(command_type_str);
+		    String meta_name = tokenizer.nextToken();
+		    new_command.append(' ');
+		    new_command.append(meta_name);
+		    while (tokenizer.hasMoreTokens()) {
+			new_command.append(' ');
+			new_command.append(tokenizer.nextToken());
+		    }
+		    if (meta_name.equals(StaticStrings.COLLECTIONMETADATA_COLLECTIONNAME_STR) || meta_name.equals(StaticStrings.COLLECTIONMETADATA_COLLECTIONEXTRA_STR) || meta_name.equals(StaticStrings.COLLECTIONMETADATA_ICONCOLLECTION_STR) || meta_name.equals(StaticStrings.COLLECTIONMETADATA_ICONCOLLECTIONSMALL_STR)) {
+			// dont save
+		    } else {
+			write(out, new_command.toString());
+		    }
+		    new_command = null;
+		    continue;
+		} // if collectionmeta
+
+		if (command_type_str.equals("classify")) {
+		    StringBuffer text = new StringBuffer(command_type_str);
+		    // Read in the classifier command watching for hfile, metadata and sort arguments.
+		    String buttonname = null;
+		    String hfile = null;
+		    String new_metadata = null;
+		    String old_metadata = null;
+		
+		    while(tokenizer.hasMoreTokens()) {
+			String token = tokenizer.nextToken();
+			if (token.equals("-hfile")) {
+			    if(tokenizer.hasMoreTokens()) {
+				text.append(" ");
+				text.append(token);
+				token = tokenizer.nextToken();
+				hfile = token;
+			    }
+			}
+			else if (token.equals("-metadata")) {
+			    if(tokenizer.hasMoreTokens()) {
+				text.append(" ");
+				text.append(token);
+				String temp_metadata = tokenizer.nextToken();
+				String replacement = ProfileXMLFileManager.getMetadataElementFor(temp_metadata);
+				if (replacement != null && !replacement.equals("")) {
+				    token = replacement;
+				    old_metadata = temp_metadata;
+				    new_metadata = replacement;
+				}
+				else {
+				    token = temp_metadata;
+				}
+				temp_metadata = null;
+				replacement = null;
+			    }
+			}
+			else if (token.equals("-sort")) {
+			    if(tokenizer.hasMoreTokens()) {
+				text.append(" ");
+				text.append(token);
+				String temp_metadata = tokenizer.nextToken();
+				String replacement = ProfileXMLFileManager.getMetadataElementFor(temp_metadata);
+				if (replacement != null && !replacement.equals("")) {
+				    token = replacement;
+				}
+				else {
+				    token = temp_metadata;
+				}
+				temp_metadata = null;
+				replacement = null;
+			    }
+			}
+			else if(token.equals("-buttonname")) {
+			    buttonname = token;
+			}
+			text.append(' ');
+			text.append(token);
+			token = null;
+		    }
+		   
+		    // If we replaced the metadata argument and didn't encounter a buttonname, then add one now pointing back to the old metadata name in order to accomodate macro files which required such names (buttonname is metadata name by default)!
+		    if(old_metadata != null && new_metadata != null && buttonname == null) {
+			text.append(' ');
+			text.append("-buttonname");
+			text.append(' ');
+			text.append(old_metadata);
+		    }
+		    command = text.toString();
+		    // Replace the hfile if we found it
+		    if(hfile != null && new_metadata != null) {
+			command = command.replaceAll(hfile, new_metadata + ".txt");
+		    }
+		
+		    buttonname = null;
+		    hfile = null;
+		    new_metadata = null;
+		    old_metadata = null;
+		    write(out, command);
+		} else {
+		    // the rest of the commands just want a string - we read in all the tokens from the tokeniser and get rid of it.
+		    StringBuffer new_command = new StringBuffer(command_type_str);
+		    while (tokenizer.hasMoreTokens()) {
+			new_command.append(' ');
+			new_command.append(tokenizer.nextToken());
+		    }
+
+		    command = new_command.toString();
+		    
+		    // There is still one special case, that of the format command. In such a command we have to search for [<target>] to ensure we don't change parts of the format which have nothing to do with the metadata elements.
+		    // we really want to build up the whole command here
+		    boolean format_command = command_type_str.equals("format");
+		    HashMap metadata_mapping = ProfileXMLFileManager.getMetadataMapping();
+		    if (metadata_mapping != null) {
+			Iterator keys = metadata_mapping.keySet().iterator();
+			while (keys.hasNext()) {
+			    String target = (String) keys.next();
+			    String replacement = (String) metadata_mapping.get(target);
+			    if (replacement != null && !replacement.equals("")) {
+				if (format_command) {
+				    target = "\\[" + target + "\\]";
+				    replacement = "{Or}{[" + replacement + "]," + target + "}";
+				}
+				command = command.replaceAll(target, replacement);
+			    }
+			}
+		    }
+
+		    write(out, command);
+		}
+		tokenizer = null;
+	    }
+	    in.close();
+	    in = null;
+	    out.flush();
+	    out.close();
+	    out = null;
+	}
+	catch(Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+	// All done, I hope.
+    }
+    
+    private void write(BufferedWriter out, String message)
+	throws Exception {
+	out.write(message, 0, message.length());
+	out.newLine();
+    }
+
+
+    /** The CollectionManager class is getting too confusing by half so I'll implement this TreeModelListener in a private class to make responsibility clear. */
+    private class FMTreeModelListener
+	implements TreeModelListener {
+	/** Any action that changes one of the tree models within a collection, which are the only models we listen to, mean the collections contents have changed and so saved should be set to false.
+	 * @param event A <strong>TreeModelEvent</strong> encompassing all the information about the event which has changed the tree.
+	 */
+	public void treeNodesChanged(TreeModelEvent event) {
+	    if(collection != null) {
+		collection.setSaved(false);
+		collection.setFilesChanged(true);
+	    }
+	}
+	/** Any action that changes one of the tree models within a collection, which are the only models we listen to, mean the collections contents have changed and so saved should be set to false.
+	 * @param event A <strong>TreeModelEvent</strong> encompassing all the information about the event which has changed the tree.
+	 */
+	public void treeNodesInserted(TreeModelEvent event) {
+	    if(collection != null) {
+		collection.setSaved(false);
+		collection.setFilesChanged(true);
+	    }
+	}
+	/** Any action that changes one of the tree models within a collection, which are the only models we listen to, mean the collections contents have changed and so saved should be set to false.
+	 * @param event A <strong>TreeModelEvent</strong> encompassing all the information about the event which has changed the tree.
+	 */
+	public void treeNodesRemoved(TreeModelEvent event) {
+	    if(collection != null) {
+		collection.setSaved(false);
+		collection.setFilesChanged(true);
+
+	    }
+	}
+	/** Any action that changes one of the tree models within a collection, which are the only models we listen to, mean the collections contents have changed and so saved should be set to false.
+	 * @param event A <strong>TreeModelEvent</strong> encompassing all the information about the event which has changed the tree.
+	 */
+	public void treeStructureChanged(TreeModelEvent event) {
+	    if(collection != null) {
+		collection.setSaved(false);
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionTree.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionTree.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionTree.java	(revision 31635)
@@ -0,0 +1,444 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.collection;
+
+import java.awt.*;
+import java.awt.dnd.DropTargetDropEvent;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.tree.DragTree;
+import org.greenstone.gatherer.gui.tree.DragTreeCellRenderer;
+
+
+public class CollectionTree
+    extends DragTree
+    implements MouseListener
+{
+    public CollectionTree(CollectionTreeModel collection_tree_model, boolean mixed_selection)
+    {
+	super(collection_tree_model, mixed_selection);
+	addMouseListener(this);
+
+	setCellRenderer(new CollectionTreeCellRenderer());
+	setBackgroundNonSelectionColor(Configuration.getColor("coloring.collection_tree_background", false));
+	setBackgroundSelectionColor(Configuration.getColor("coloring.collection_selection_background", false));
+	setTextNonSelectionColor(Configuration.getColor("coloring.collection_tree_foreground", false));
+	setTextSelectionColor(Configuration.getColor("coloring.collection_selection_foreground", false));
+
+	filter.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+	filter.setEditable(Configuration.getMode() >= Configuration.LIBRARIAN_MODE);
+    }
+
+
+    public boolean isDraggable()
+    {
+	return true;
+    }
+
+
+    public boolean isDroppable()
+    {
+	return true;
+    }
+
+	// Overridden here: when a successful drag and drop takes place 
+	// make sure nothing is selected in the tree, so that no valueChanged()
+	// event gets fired anymore by items getting reselected in the tree 
+	// (this  used to result in the metadata table in EnrichPane being updated 
+	// upon its valueChanged() getting called and funny things happened to
+	// the metadata due to state inconsistencies). 
+	public void drop(DropTargetDropEvent event) {
+		if (!isDroppable()) {
+			return;
+		}
+		
+		setImmediate(true);
+		clearSelection();
+		setImmediate(false);
+		
+		// let the superclass' drop() process the rest of the drag event
+		super.drop(event);
+		
+	}
+
+    public void mouseClicked(MouseEvent event)
+    {
+	if (SwingUtilities.isRightMouseButton(event)) {
+	    new CollectionTreeRightClickMenu(this, event);
+	}
+    }
+
+    public void mouseEntered(MouseEvent event) { }
+
+    public void mouseExited(MouseEvent event) { }
+
+    public void mousePressed(MouseEvent event) { }
+
+    public void mouseReleased(MouseEvent event) { }
+
+
+    public String toString()
+    {
+	return "Collection";
+    }
+
+
+    private class CollectionTreeCellRenderer
+	extends DragTreeCellRenderer
+    {
+	public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus)
+	{
+	    JLabel tree_cell = (JLabel) super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
+
+	    // Mark explodable files and SrcReplaceable files with a different icon (Green file icon)
+	    if(value instanceof CollectionTreeNode) {
+		if(((CollectionTreeNode) value).isExplodable() || ((CollectionTreeNode) value).isSrcReplaceable()) {
+		    tree_cell.setIcon(CollectionTreeNode.GREEN_FILE_ICON);
+		}
+	    }
+	        
+	    return tree_cell;
+	}
+    }
+
+
+    /** When a user right-clicks within the workspace and collection trees they are presented with a small popup menu of context based options. This class provides such functionality.
+     */
+    private class CollectionTreeRightClickMenu
+	extends JPopupMenu
+	implements ActionListener
+    {
+	/** The tree over which the right click action occurred. */
+	private CollectionTree collection_tree = null;
+	/** The tree nodes selected when the right click action occurred. */
+	private TreePath[] selection_paths = null;
+	/** The file record over which the right click action occurred. */
+	private CollectionTreeNode node = null;
+
+	private JMenuItem collapse_folder = null;
+	private JMenuItem expand_folder = null;
+	private JMenuItem explode_metadata_database = null;
+	private JMenuItem replace_srcdoc_with_html = null;
+	private JMenuItem delete = null;
+	private JMenuItem metaaudit = null;
+	private JMenuItem new_folder = null;
+	private JMenuItem new_dummy_doc = null;
+	private JMenuItem refresh = null; // for refreshing folder view
+	private JMenuItem open_externally = null;
+	private JMenuItem rename = null;
+	private JMenuItem replace = null;
+
+
+	private CollectionTreeRightClickMenu(CollectionTree collection_tree, MouseEvent event)
+	{
+	    super();
+	    this.collection_tree = collection_tree;
+
+	    // Note we have to use setImmediate() with the set selction paths
+	    // otherwise the selection doesn't get updated until after the 
+	    // popup comes up.
+
+	    // the right click position
+	    TreePath right_click_path = collection_tree.getPathForLocation(event.getX(), event.getY());
+	    if (right_click_path == null) {
+		// user has clicked outside of the tree, clear the selection
+		selection_paths = null;
+		collection_tree.setImmediate(true);
+		collection_tree.clearSelection();
+		collection_tree.setImmediate(false);
+	    }
+	    else {
+		// Get the paths currently selected in the tree
+		selection_paths = collection_tree.getSelectionPaths();
+		if (selection_paths == null) {
+		    // nothing currently selected - we shift the selection to 
+		    // the node that was right clicked on
+		    selection_paths = new TreePath[1];
+		    selection_paths[0] = right_click_path;
+		    collection_tree.setImmediate(true);
+		    collection_tree.setSelectionPath(right_click_path);
+		    collection_tree.setImmediate(false);
+		}
+		else if (selection_paths.length == 1 && ! selection_paths[0].equals( right_click_path)) {
+		    collection_tree.setImmediate(true);
+		    collection_tree.clearSelection();
+		    collection_tree.setSelectionPath(right_click_path);
+		    collection_tree.setImmediate(false);
+		    selection_paths[0] = right_click_path;
+		}
+		else {
+		    // we had multiply selected paths in the tree.
+		    // if we clicked on one of those paths, then use all the 
+		    // current selection, otherwise clear the selection and 
+		    // select the one we right clicked on
+		    boolean clicked_in_selection = false;
+		    for (int i = 0; i < selection_paths.length; i++) {
+			if (selection_paths[i].equals(right_click_path)) {
+			    clicked_in_selection = true;
+			    break;
+			}
+		    }
+		    if (!clicked_in_selection) {
+			// want the tree to update right away
+			collection_tree.setImmediate(true);
+			collection_tree.clearSelection();
+			collection_tree.setSelectionPath(right_click_path);
+			collection_tree.setImmediate(false);
+			selection_paths = new TreePath[1];
+			selection_paths[0] = right_click_path;
+		    }
+		}
+	    }
+	    
+	    // Create an appropriate context menu, based on what is selected
+	    buildContextMenu(selection_paths);
+
+	    // Show the popup menu on screen
+	    show(collection_tree, event.getX(), event.getY());
+	}
+
+	/** Checks whether the files selected are of the same filetype (have the same extension).
+	 * @param selectedFilePaths - the full file paths to the treenodes selected in the 
+	 * collection fileview. 
+	 * @return true if the file extensions of all the selected files are the same. False is 
+	 * returned if the file extension of any selected file is different (this means that if 
+	 * a folder was selected, false would be returned). False is also returned if nothing was
+	 * selected.
+	 * For use with replace_srcdoc_with_html.pl
+	*/
+	private boolean selectedFilesOfSameType(TreePath[] selectedFilePaths) {
+	    if(selectedFilePaths == null || selectedFilePaths.length <= 0)
+		return false;
+
+	    boolean sameExtension = true;
+
+	    // get just the filename from the path and extract its extension
+	    String firstFile = selectedFilePaths[0].getLastPathComponent().toString();
+	    int period = firstFile.lastIndexOf('.');
+	    if(period == -1) { // someone could have selected a folder
+		return false;
+	    }
+	    String extension = firstFile.substring(period); // includes period
+
+	    // compare with the other selected files' extensions:
+	    for(int i = 1; i < selectedFilePaths.length && sameExtension; i++) {
+		String otherFile = selectedFilePaths[i].getLastPathComponent().toString();
+		String otherFileExt = otherFile.substring(otherFile.lastIndexOf('.'));
+		if(!extension.equals(otherFileExt)) 
+		    sameExtension = false;
+	    }
+	    return sameExtension;
+	}
+
+	private void buildContextMenu(TreePath[] selection_paths)
+	{
+	    // If nothing is selected, only the new folder/dummy doc options are available...
+	    if (selection_paths == null) {
+		new_folder = new JMenuItem(Dictionary.get("CollectionPopupMenu.New_Folder"), KeyEvent.VK_N);
+		new_folder.addActionListener(this);
+		add(new_folder);
+
+		new_dummy_doc = new JMenuItem(Dictionary.get("CollectionPopupMenu.New_Dummy_Doc"));
+		new_dummy_doc.addActionListener(this);
+		add(new_dummy_doc);
+
+		refresh = new JMenuItem(Dictionary.get("CollectionPopupMenu.Refresh"));
+		if(Gatherer.isGsdlRemote) {
+		    refresh.setEnabled(false);
+		}
+		refresh.addActionListener(this);
+		add(refresh);
+
+		node = (CollectionTreeNode) collection_tree.getModel().getRoot();
+		return;
+	    }
+
+	    // Meta-audit and delete options
+	    metaaudit = new JMenuItem(Dictionary.get("Menu.Metadata_View", collection_tree.getSelectionDetails()), KeyEvent.VK_A);
+	    metaaudit.addActionListener(this);
+	    add(metaaudit);
+
+	    delete = new JMenuItem(Dictionary.get("CollectionPopupMenu.Delete"), KeyEvent.VK_D);
+	    delete.addActionListener(this);
+	    add(delete);
+
+	    // The src doc replaceable (with html file) option is only available when all files selected are of the
+	    // same type (same extension). For srcreplaceable files only. Works with replace_srcdoc_with_html.pl
+	    CollectionTreeNode firstSelectedNode = (CollectionTreeNode)selection_paths[0].getLastPathComponent();
+	    if(firstSelectedNode.isSrcReplaceable()) { // test the first selected node
+		replace_srcdoc_with_html = new JMenuItem(Dictionary.get("Menu.Replace_SrcDoc_With_HTML"), KeyEvent.VK_H);
+		replace_srcdoc_with_html.addActionListener(this);		
+		add(replace_srcdoc_with_html);
+		
+		// Now the menu is there, grey it out if not all the files are of the same type
+		if(!selectedFilesOfSameType(selection_paths)) {
+		    replace_srcdoc_with_html.setEnabled(false);
+		} 
+	    }
+	   
+	    // Only meta-audit and delete (and possibly replace_srcdoc) are available if multiple items are selected...
+	    if (selection_paths.length > 1) {
+		return;
+	    }
+
+	    // Rename option
+	    rename = new JMenuItem(Dictionary.get("CollectionPopupMenu.Rename"), KeyEvent.VK_R);
+	    rename.addActionListener(this);
+	    add(rename);
+
+	    TreePath path = selection_paths[0];
+	    node = (CollectionTreeNode) path.getLastPathComponent();
+
+	    // ---- Options for file nodes ----
+	    if (node.isLeaf()) {
+		// Explode metadata databases, for explodable files only
+		if (node.isExplodable()) {
+		    explode_metadata_database = new JMenuItem(Dictionary.get("Menu.Explode_Metadata_Database"), KeyEvent.VK_E);
+		    explode_metadata_database.addActionListener(this);
+		    add(explode_metadata_database);
+		}
+	
+		// Replace file
+		replace = new JMenuItem(Dictionary.get("CollectionPopupMenu.Replace"), KeyEvent.VK_P);
+		replace.addActionListener(this);
+		add(replace);
+		// Open the file in an external program
+		open_externally = new JMenuItem(Dictionary.get("Menu.Open_Externally"), KeyEvent.VK_O);
+		open_externally.addActionListener(this);
+		add(open_externally);
+
+		return;
+	    }
+
+	    // ---- Options for folder nodes ----
+	    // Collapse or expand, depending on current status
+	    if (collection_tree.isExpanded(path)) {
+		collapse_folder = new JMenuItem(Dictionary.get("Menu.Collapse"), KeyEvent.VK_C);
+		collapse_folder.addActionListener(this);
+		add(collapse_folder);
+	    }
+	    else {
+		expand_folder = new JMenuItem(Dictionary.get("Menu.Expand"), KeyEvent.VK_O);
+		expand_folder.addActionListener(this);
+		add(expand_folder);
+	    }
+
+	    // New folder/dummy doc options
+	    if (!node.isReadOnly()) {
+		new_folder = new JMenuItem(Dictionary.get("CollectionPopupMenu.New_Folder"), KeyEvent.VK_N);
+		new_folder.addActionListener(this);
+		add(new_folder);
+
+		new_dummy_doc = new JMenuItem(Dictionary.get("CollectionPopupMenu.New_Dummy_Doc"));
+		new_dummy_doc.addActionListener(this);
+		add(new_dummy_doc);
+	    }
+	}
+
+
+	/** Called whenever one of the menu items is clicked, this method then causes the appropriate effect. */
+	public void actionPerformed(ActionEvent event)
+	{	
+	    Object source = event.getSource();
+
+	    // Collapse folder
+	    if (source == collapse_folder) {
+		collection_tree.collapsePath(selection_paths[0]);
+	    }
+
+	    // Expand folder
+	    else if (source == expand_folder) {
+		collection_tree.expandPath(selection_paths[0]);
+	    }
+
+	    // Explode metadata database
+	    else if (source == explode_metadata_database) {
+ 		Gatherer.f_man.explodeMetadataDatabase(node.getFile());
+	    }
+	    
+	    // Replace source document with generated html (works with replace_srcdoc_with_html.pl)
+	    else if (source == replace_srcdoc_with_html) {
+		java.io.File[] source_files = new java.io.File[selection_paths.length];
+		for (int i = 0; i < selection_paths.length; i++) {
+		    CollectionTreeNode node = (CollectionTreeNode) selection_paths[i].getLastPathComponent(); 
+		    source_files[i] = node.getFile();
+		}
+		Gatherer.f_man.replaceSrcDocWithHtml(source_files); // passing the selected files
+	    }
+
+	    // Delete
+	    else if (source == delete) {
+		CollectionTreeNode[] source_nodes = new CollectionTreeNode[selection_paths.length];
+		for (int i = 0; i < selection_paths.length; i++) {
+		    source_nodes[i] = (CollectionTreeNode) selection_paths[i].getLastPathComponent();
+		}
+
+		// Fire a delete action
+		Gatherer.f_man.action(collection_tree, source_nodes, Gatherer.recycle_bin, null);
+	    }
+
+	    // Meta-audit
+	    else if (source == metaaudit) {
+ 		Gatherer.g_man.showMetaAuditBox();
+ 	    }
+
+	    // New folder
+  	    else if (source == new_folder) {
+ 		Gatherer.f_man.newFolder(collection_tree, node);
+ 	    }
+
+	    // New dummy doc
+	    else if (source == new_dummy_doc) {
+		Gatherer.f_man.newDummyDoc(collection_tree, node);
+	    }
+
+	    // Refresh action to reload folder view
+	    else if (source == refresh) { // Refresh collection tree
+		Gatherer.g_man.refreshCollectionTree(DragTree.COLLECTION_CONTENTS_CHANGED);
+	    }
+
+	    // Open in external program
+	    else if (source == open_externally) {
+		Gatherer.f_man.openFileInExternalApplication(node.getFile());
+	    }
+
+	    // Rename
+	    else if (source == rename) {
+		Gatherer.f_man.renameCollectionFile(collection_tree, node);
+	    }
+
+	    // Replace
+	    else if (source == replace) {
+		Gatherer.f_man.replaceCollectionFile(collection_tree, node);
+	    }
+  	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionTreeModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionTreeModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionTreeModel.java	(revision 31635)
@@ -0,0 +1,41 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.collection;
+
+
+import org.greenstone.gatherer.file.FileSystemModel;
+
+
+/** This class represents ... */
+public class CollectionTreeModel
+    extends FileSystemModel
+{
+    public CollectionTreeModel(CollectionTreeNode root_node)
+    {
+	super(root_node);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionTreeNode.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionTreeNode.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/CollectionTreeNode.java	(revision 31635)
@@ -0,0 +1,224 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.collection;
+
+
+import java.io.*;
+import javax.swing.*;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.file.FileNode;
+import org.greenstone.gatherer.metadata.FilenameEncoding;
+import org.greenstone.gatherer.util.JarTools;
+import java.util.Set;
+import java.util.Iterator;
+
+
+/** This class represents one node in the collection tree. */
+public class CollectionTreeNode
+    extends FileNode
+{
+    static final public ImageIcon GREEN_FILE_ICON = JarTools.getImage("greenfile.gif", true);
+    
+    /** Is this file a metadata database that is explodable with the explode_metadata_databases.pl script? */
+    private boolean is_explodable = false;
+
+    /** Keeps track of whether this file can be replaced with a Greenstone-generated html file.
+     * To work with replace_srcdoc_with_html.pl */
+    private boolean is_srcreplaceable = false;
+
+    public CollectionTreeNode(File file)
+    {
+	super(file);
+		// the super call will additionally call calcDisplayString() which will get any
+		// applicable the filename encoding and apply it to the file's name for display.
+
+	this.is_explodable = CollectionDesignManager.plugin_manager.isFileExplodable(file);
+	// To work with replace_srcdoc_with_html.pl
+	this.is_srcreplaceable = CollectionDesignManager.plugin_manager.isFileSrcReplaceable(file);
+    
+    }
+
+
+    public FileNode addChildNode(File file)
+    {
+	CollectionTreeNode child_node = new CollectionTreeNode(file);
+	child_node.setModel(model);
+	child_node.setParent(this);
+	return child_node;
+    }
+
+
+    public boolean isExplodable()
+    {
+	return is_explodable;
+    }
+
+    // To work with replace_srcdoc_with_html.pl
+    public boolean isSrcReplaceable()
+    {
+	return is_srcreplaceable;
+    }
+
+	/** This method returns a string representation of the filenodes in the 
+	* <i>Collection</i> Tree, that can then be displayed in the tree. If the 
+	* filename encoding is specified as metadata with the file, then this
+	* method will apply that to the filename's bytes to get the displayName.
+	*/
+    protected String calcDisplayString() {		
+		// metadata.xml files in collections don't get displayed anyway
+		if(file.getName().equals("metadata.xml")) { 
+		    return super.calcDisplayString();
+		}
+		
+		if(!FilenameEncoding.MULTIPLE_FILENAME_ENCODINGS_SUPPORTED) {
+		    return super.calcDisplayString();
+		}
+
+		// Else: try to encode the display string from the 
+		// file_to_encoding map. If that fails, just use the locale	
+		String displayName = null;
+		String urlEncodedPath = getURLEncodedFilePath();
+		String encoding = FilenameEncoding.findFilenameEncoding(file, urlEncodedPath, false);
+
+		// if it isn't the same as the current encoding already applied, reapply
+		// it which will set the filenameEncoding again as well
+		if(!encoding.equals(getFilenameEncoding())) {
+		    displayName = reencodeDisplayName(encoding); // may return null
+		}
+		
+		if(displayName == null) {
+			if(FilenameEncoding.DEBUGGING) {
+				displayName = getURLEncodedFileName(); 
+			} else {
+				displayName = super.calcDisplayString();
+			}
+		}
+		
+		return displayName;
+    }
+
+	/** Call this if the filename encoding has changed and needs to be 
+	* recalculated and re-applied to the filename. This is only for display,
+	* so it only gets applied to the urlEncodedFileName (not urlEncodedFilePath). 
+	* Note that the filenameEncoding may not be the same as the given encoding
+	* at the end of this method (in case it was a charset alias of the encoding). 
+	* This method both sets and returns the displayFileName member variable. 
+	* @return the reencoded display name, if the encoding was a recognised alias
+	* in which case the filenameEncoding will store the canonical name.  */
+    public String reencodeDisplayName(String encoding) {
+		if(!FilenameEncoding.MULTIPLE_FILENAME_ENCODINGS_SUPPORTED) {
+		    return displayFileName; // may still be null
+		}
+
+		filenameEncoding = encoding; // clear previous value
+
+		if(filenameEncoding.equals("")) {
+		    displayFileName = super.calcDisplayString(); // *FileNode* calcDisplayString
+		    return displayFileName;
+		}
+
+		try{
+		    displayFileName = new String(file.getName().getBytes(), filenameEncoding);
+		} catch(Exception e) { 
+			// IllegalCharsetName-, UnsupportedCharset- or UnsupportedEncoding-Exception
+		    // Store the unsupported encoding, but display with filesystem (or URL) encoding
+		    filenameEncoding = encoding;
+			
+			if(FilenameEncoding.DEBUGGING) {
+				displayFileName = getURLEncodedFileName();
+			} else {
+				displayFileName = super.calcDisplayString(); 
+			}
+		} 
+		return displayFileName;
+    }
+		
+	/** Can call this upon refreshing a FileNode (this CollectionTreeNode). It makes
+	* sure the display names of the visible nodes in the CollectionTree aren't stale */
+    public void refreshDescendantEncodings() {
+		// now recalculate encodings for all visible children
+		
+		// Don't bother with the encoding stuff when multiple filename encodings 
+		// are not supported because the system is UTF-8 (not Native-Latin-1) and 
+		// Java interprets all filename bytes as UTF-8, which destructively 
+		// converts unrecognised characters (for UTF-8) into the invalid character.	
+
+		if(!FilenameEncoding.MULTIPLE_FILENAME_ENCODINGS_SUPPORTED 
+				|| child_nodes == null) 
+		{
+		    return;
+		}		
+		
+		// get and apply the filename encoding, if any
+		for(int i = 0; i < child_nodes.size(); i++) {
+		    CollectionTreeNode child_node = (CollectionTreeNode) child_nodes.get(i);			
+		    String urlEncodedPath = child_node.getURLEncodedFilePath();		
+		    
+		    String encoding = FilenameEncoding.findFilenameEncoding(
+					child_node.getFile(), urlEncodedPath, false);
+		    
+		    // if current encoding is different from the existing one, re-apply encoding
+		    if(!child_node.getFilenameEncoding().equals(encoding)) {
+			child_node.reencodeDisplayName(encoding);
+		    }
+		}
+    }
+	
+	// Unused at present
+	/** Call when the filename encoding of this folder level fileNode has changed
+	* and therefore the filename encodings of the descendant fileNodes have to be
+	* reset (their entries in the file_to_encoding hashmap cleared), so that we know
+	* to recalculate these later.  Call when deleting, moving or renaming col nodes */
+    public void resetDescendantEncodings() {
+		if(FilenameEncoding.MULTIPLE_FILENAME_ENCODINGS_SUPPORTED) {
+		    resetDescendantEncodings(this.file, this.getURLEncodedFilePath());
+		    
+		    // for an actual file/folder delete, want to remove all corresponding 
+			// entries from the hashtable without recalculating any encodings 
+			// (files have been deleted, so no there will be no filenames to display).
+			// So don't do this here: refreshDescendantEncodings();
+			// It's done near the end of FileNode.map().
+		}
+    }
+	
+	// Together with the above, unused at present
+    private static void resetDescendantEncodings(File f, String urlEncodedPath) 
+    {
+		// remove this file f's urlencoded path name from the file_to_encoding Map
+		FilenameEncoding.map.remove(urlEncodedPath);
+			
+		if(f.isDirectory()) {
+		    File[] children = f.listFiles();	    
+		    for(int i = 0; i < children.length; i++) {
+				urlEncodedPath = FilenameEncoding.fileToURLEncoding(children[i]);
+				resetDescendantEncodings(children[i], urlEncodedPath); 
+						// sets filenameEncoding var
+	 	    }
+		}
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/LegacyCollectionImporter.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/LegacyCollectionImporter.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/LegacyCollectionImporter.java	(revision 31635)
@@ -0,0 +1,144 @@
+package org.greenstone.gatherer.collection;
+
+
+import java.io.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.Argument;
+import org.greenstone.gatherer.cdm.Classifier;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataTools;
+import org.greenstone.gatherer.metadata.ProfileXMLFileManager;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+
+/**
+ * Importing legacy collections involves two main steps:
+ *
+ * 1. Read the existing metadata.xml files and namespace them, usually by prompting the user
+ *    to specify the mapping between old metadata elements and new elements.
+ *
+ * 2. Fix up the classify commands in the collect.cfg file to specify the new element names.
+ *
+ * @author Michael Dewsnip
+ */
+public class LegacyCollectionImporter 
+{
+    /** Definition of an important directory name, in this case the backup import directory for the collection. */
+    static final public String IMPORT_BAK_DIR = "import.bak" + File.separator;
+
+
+    // This copies all the existing metadata.xml files into a backup directory
+    static public void backupMetadataXMLFiles(File collection_dir)
+    {
+	File import_dir = new File(collection_dir, "import");
+	File import_bak_dir = new File(collection_dir, IMPORT_BAK_DIR);
+	import_bak_dir.mkdir();
+	copyMetadataXMLFiles(import_dir, import_bak_dir);
+    }
+
+
+    static private void copyMetadataXMLFiles(File source_dir, File backup_dir)
+    {
+	if (source_dir == null || !source_dir.exists()) {
+	    return;
+	}
+
+	// Find the metadata file in this dir
+	File metadata_xml_file_file = new File(source_dir, "metadata.xml");
+	if (metadata_xml_file_file.exists()) {
+	    File new_metadata_xml_file_file = new File(backup_dir, "metadata.xml");
+	    try {
+		backup_dir.mkdirs();
+		Gatherer.f_man.getQueue().copyFile(metadata_xml_file_file, new_metadata_xml_file_file, false);
+		if (!new_metadata_xml_file_file.exists()) {
+		    throw new Exception("");
+		}
+	    }
+	    catch (Exception e) {
+		DebugStream.println("Exception: couldn't copy the file " + metadata_xml_file_file.getPath() + e.getMessage());
+	    }
+	}
+
+	// Now go through child directories
+	File [] children = source_dir.listFiles();
+	for (int i = 0; i < children.length; i++) {
+	    File child = children[i];
+	    if (child.isDirectory()) {
+		copyMetadataXMLFiles(child, new File(backup_dir, child.getName()));
+	    }
+	}
+    }
+
+
+    static public void updateClassifiers(CollectionDesignManager cdm)
+    {
+	// Update the metadata elements in each of the classifiers
+	for (int i = 0; i < cdm.classifier_manager.getSize(); i++) {
+	    Classifier classifier = cdm.classifier_manager.getClassifier(i);
+	    // System.err.println("Classifier: " + classifier);
+
+	    // Update the "-metadata" value
+	    Argument metadata_argument = classifier.getArgument("-metadata");
+	    String metadata_argument_value = metadata_argument.getValue();
+	    String metadata_element_name_full = mapLegacyMetadataElementName(metadata_argument_value);
+	    if (metadata_element_name_full != null && !metadata_element_name_full.equals("")) {
+		String metadata_element_display_name_full = MetadataTools.getDisplayNameForMetadataElementWithName(metadata_element_name_full);
+		if (metadata_element_display_name_full != null) {
+		    metadata_argument_value = metadata_element_display_name_full;
+		    metadata_argument.setValue(metadata_argument_value);
+		}
+	    }
+
+  	    // Update the "-sort" value
+	    Argument sort_argument = classifier.getArgument("-sort");
+	    if (sort_argument != null) {
+		String sort_element_name_full = mapLegacyMetadataElementName(sort_argument.getValue());
+		if (sort_element_name_full != null && !sort_element_name_full.equals("")) {
+		    String sort_element_display_name_full = MetadataTools.getDisplayNameForMetadataElementWithName(sort_element_name_full);
+		    if (sort_element_display_name_full != null) {
+			sort_argument.setValue(sort_element_display_name_full);
+		    }
+		}
+	    }
+
+	    // Set the "-buttonname" value if it isn't already set
+	    Argument buttonname_argument = classifier.getArgument("-buttonname");
+	    if (buttonname_argument != null && buttonname_argument.getValue().equals("")) {
+		int namespace_index = metadata_argument_value.indexOf(".");
+		if (namespace_index != -1) {
+		    buttonname_argument.setValue(metadata_argument_value.substring(namespace_index + 1));
+		}
+		else {
+		    buttonname_argument.setValue(metadata_argument_value);
+		}
+		buttonname_argument.setAssigned(true);
+	    }
+
+	    // With Hierarchy classifiers, update the hfile arguments
+  	    if (classifier.getName().equalsIgnoreCase(StaticStrings.HIERARCHY_CLASSIFIER)) {
+		// Update the "-hfile" value
+		Argument hfile_argument = classifier.getArgument(StaticStrings.HFILE_ARGUMENT);
+		hfile_argument.setValue(metadata_element_name_full + ".txt");
+  	    }
+
+	    // System.err.println("Classifier (after): " + classifier);
+	}
+    }
+
+
+    static private String mapLegacyMetadataElementName(String metadata_element_name)
+    {
+ 	// Remove the extracted namespace if it has been added, but only
+	// if there are no subsequent namespaces following it (like ex.dc.*)
+	// since we want to preserve embedded metadata names as is.
+ 	if (metadata_element_name.startsWith(StaticStrings.EXTRACTED_NAMESPACE) && metadata_element_name.indexOf(StaticStrings.NS_SEP, StaticStrings.EXTRACTED_NAMESPACE.length()) == -1) {
+ 	    metadata_element_name = metadata_element_name.substring(StaticStrings.EXTRACTED_NAMESPACE.length());
+ 	}
+
+ 	// Update the metadata value to the new (namespaced) one
+	return ProfileXMLFileManager.getMetadataElementFor(metadata_element_name);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/ScriptOptions.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/ScriptOptions.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/collection/ScriptOptions.java	(revision 31635)
@@ -0,0 +1,273 @@
+package org.greenstone.gatherer.collection;
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.Argument;
+import org.greenstone.gatherer.greenstone.LocalGreenstone;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.ArrayTools;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+
+/** This class parses options from Perl scripts: import.pl, buildcol.pl and explode_metadata_database.pl in particular. */
+public class ScriptOptions
+{
+    /** The root element of the argument tree. */
+    private Element arguments_element;
+    /** The root element of the values tree. */
+    private Element values_element;
+    /** The name of an argument element. */
+    static final private String ARGUMENT = "Argument";
+    /** The name of the enabled attribute. */
+    static final private String ENABLED = "enabled";
+    /** The name of the 'false' value. */
+    static final private String FALSE = "false";
+    /** The name of a name attribute. */
+    static final private String NAME = "name";
+    /** The name of an option element. */
+    static final private String OPTION = "Option";
+    /** The name of the 'true' value. */
+    static final private String TRUE = "true";
+
+
+    /** Parse the arguments from running "script_name -xml" */
+    public ScriptOptions(Element values_element, String script_name)
+    {
+	this.values_element = values_element;
+	this.arguments_element = loadArguments(script_name, Configuration.getLanguage());
+    }
+
+
+    /** Retrieve the indexth argument */
+    public Argument getArgument(int index)
+    {
+	Argument argument = null;
+
+	NodeList option_list = arguments_element.getElementsByTagName(OPTION);
+	if (index >= 0 && index < option_list.getLength()) {
+	    argument = new Argument();
+	    argument.parseXML((Element) option_list.item(index));
+	}
+
+	return argument;
+    }
+
+
+    /** Retrieve the number of arguments.
+     */
+    public int getArgumentCount() {
+	// Determining the total count is easy.
+	NodeList argument_elements = arguments_element.getElementsByTagName(OPTION);
+	return argument_elements.getLength();
+    }
+
+
+    /** Retrieve the value of a certain argument. */
+    public String getValue(String name) {
+	return getValue(name, false);
+    }
+
+    /** Determine if the named argument value is enabled or disabled. */
+    public boolean getValueEnabled(String name) {
+	boolean result = false;
+	String value = getValue(name, true);
+	if(value != null && value.length() > 0) {
+	    result = (value.equalsIgnoreCase(TRUE));
+	}
+	return result;
+    }
+
+    /** Retrieve all of the values as a String array ready to added to the script call. */
+    public String[] getValues() {
+	ArrayList values = new ArrayList();
+	try {
+	    NodeList arguments = values_element.getElementsByTagName(ARGUMENT);
+	    for(int i = 0; i < arguments.getLength(); i++) {
+		Element argument_element = (Element) arguments.item(i);
+		// Determine if this argument is enabled.
+		if(argument_element.getAttribute(ENABLED).equalsIgnoreCase(TRUE)) {
+		    // First store the name of the argument prefixed with a '-'
+		    values.add("-" + argument_element.getAttribute(NAME));
+		    // Now retrieve the value.
+		    String argument_value = Codec.transform(XMLTools.getValue(argument_element), Codec.DOM_TO_TEXT);
+		    // If there is a value, tokenize it by commas only.
+		    if(argument_value != null && argument_value.length() > 0) {
+			values.add(argument_value);
+		    }
+		    argument_value = null;
+		}
+		argument_element = null;
+	    }
+	    arguments = null;
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+	return ArrayTools.arrayListToStringArray(values);
+    }
+
+   /** Remove the named value from the arguments */
+    public void removeValue(String name) {
+	try {
+	    NodeList arguments = values_element.getElementsByTagName(ARGUMENT);
+	    boolean found = false;
+	    for(int i = 0; !found && i < arguments.getLength(); i++) {
+		Element argument_element = (Element) arguments.item(i);
+		// Is this the argument we want.
+		if(argument_element.getAttribute(NAME).equalsIgnoreCase(name)) {
+		    values_element.removeChild(argument_element);
+		    found = true;
+		}
+		argument_element = null;
+	    }
+	    arguments = null;
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+    }
+
+    /** Set the state of some argument. Note that value may be either a single String, an ArrayList of Strings or null. If enable is false then any existing argument for the named argument is disabled. */
+    public void setValue(String name, boolean enable, String value) {
+	///ystem.err.println("Set value: " + (arguments_element == build_values_element ? "Build" : "Import") + ", " + name + ", " + enable + ", " + value);
+	try {
+	    Document document = values_element.getOwnerDocument();
+	    NodeList arguments = values_element.getElementsByTagName(ARGUMENT);
+	    boolean found = false;
+	    for(int i = 0; i < arguments.getLength(); i++) {
+		Element argument_element = (Element) arguments.item(i);
+		// If this the argument named.
+		if(argument_element.getAttribute(NAME).equalsIgnoreCase(name)) {
+		    found = true;
+		    // Set whether this argument is enabled
+		    argument_element.setAttribute(ENABLED, (enable ? TRUE : FALSE));
+		    // Now we set the value, depending or what it is.
+		    if(value == null) {
+			// Nothing to do.
+		    }
+		    else {
+			// Remove existing text nodes.
+			while(argument_element.hasChildNodes()) {
+			    argument_element.removeChild(argument_element.getFirstChild());
+			}
+			argument_element.appendChild(document.createTextNode((String)value));
+		    }
+		}
+		argument_element = null;
+	    }
+	    // If we haven't found an instance of this argument, but should have, then add it.
+	    if(!found && (enable || value != null)) {
+		Element argument_element = document.createElement(ARGUMENT);
+		argument_element.setAttribute(NAME, name);
+		argument_element.setAttribute(ENABLED, (enable ? TRUE : FALSE));
+		// Now we set the value, depending or what it is.
+		if(value == null) {
+		    // Nothing to do.
+		}
+		else {
+		    argument_element.appendChild(document.createTextNode((String)value));
+		}
+		values_element.appendChild(argument_element);
+	    }
+	    arguments = null;
+	    document = null;
+	    // Make sure the collection knows to save.
+	    Gatherer.c_man.getCollection().setSaved(false);
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+    }
+
+ 
+    private String getValue(String name, boolean is_enabled) {
+	String result = null;
+	try {
+	    NodeList arguments = values_element.getElementsByTagName(ARGUMENT);
+	    for(int i = 0; result == null && i < arguments.getLength(); i++) {
+		Element argument_element = (Element) arguments.item(i);
+		// Is this the argument we want.
+		if(argument_element.getAttribute(NAME).equalsIgnoreCase(name)) {
+		    // Are we simply determining if this argument is enabled
+		    if(is_enabled) {
+			result = argument_element.getAttribute(ENABLED);
+		    }
+		    else {
+			String argument_value = XMLTools.getValue(argument_element);
+			if(argument_value != null) {
+			    result = argument_value;
+			}
+			argument_value = null;
+		    }
+		}
+		argument_element = null;
+	    }
+	    arguments = null;
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+	return result;
+    }
+
+
+    private Element loadArguments(String filename, String lang)
+    {
+	// Run the required program.
+	try {
+	    Document document;
+
+	    if (Gatherer.isGsdlRemote) {
+		String script_output = Gatherer.remoteGreenstoneServer.getScriptOptions(filename, "");
+		document = XMLTools.parseXML(new StringReader(script_output));
+	    }
+	    else {
+		String args[];
+		args = new String[6];
+		args[0] = Configuration.perl_path;
+		args[1] = "-S";
+		args[2] = LocalGreenstone.getBinScriptDirectoryPath() + filename;
+		args[3] = "-xml";
+		args[4] = "-language";
+		args[5] = lang;
+
+
+		// Create the process.
+		Runtime runtime = Runtime.getRuntime();
+		Process process = runtime.exec(args);
+
+		
+		//for (int i=0; i<args.length; i++) { 
+		//   System.err.print(args[i] + " ");
+		//}
+		//System.err.println("");
+		
+
+
+		document = XMLTools.parseXML(process.getErrorStream());
+		
+		if (document == null) {
+		    // command has not generated XML, script has probably failed in some way
+		    for (int i=0; i<args.length; i++) { 
+			DebugStream.print(args[i] + " ");
+		    }
+		    DebugStream.println("");
+		}
+	    }
+
+	    return document.getDocumentElement();
+	}
+	catch (Exception error) {
+	    DebugStream.println("Error in ScriptOptions.loadArguments(): " + error);
+	    DebugStream.printStackTrace(error);
+	}
+
+	return null;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/Download.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/Download.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/Download.java	(revision 31635)
@@ -0,0 +1,207 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.download;
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.w3c.dom.*;
+import org.greenstone.gatherer.cdm.*;
+
+
+/** This class is responsible for storing information from a parsed downloadinfo.pl call in such a way that it allows easy access to parsed details for the purposes of user design and specification of downloads.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class Download
+    extends ArgumentContainer
+{
+    private boolean is_abstract = false;
+
+    /** A reference to the download that this one inherits from. */
+    private Download super_download = null;
+    /** The element this download is based upon. */
+    private Element element;
+    /** A description of this download. */
+    private String description = null;
+    private String name = null;
+
+
+    public Download() {
+    }
+
+    /** Method to add an argument to this download. Only adds the argument if it isn't already present.
+     * @param argument The <strong>Argument</strong> to add.
+     */
+    public void addArgument(Argument argument) {
+	if(element == null && !contains(argument)) {
+	    add(argument);
+	    argument.setOwner(name);
+	}
+    }
+
+    /** Method to compare two downloads for ordering.
+     * @param object The download we are comparing to, as an <strong>Object</strong>.
+     * @return An <i>int</i> specifying the download order, using values as set out in String.
+     * @see java.lang.String#compareTo
+     */
+    public int compareTo(Object object) {
+	if(object == null) {
+	    return -1;
+	}
+	return toString().compareTo(object.toString());
+    }
+
+    /** Probably not used.
+     */
+    public DOMProxyListEntry create(Element element) {
+	return null;
+    }
+
+    /** Method to determine if two downloads are equal.
+     * @param object The download to test against, as an <strong>Object</strong>.
+     * @return <i>true</i> if the download names match, <i>false</i> otherwise.
+     */
+    public boolean equals(Object object) {
+	return (compareTo(object) == 0);
+    }
+
+    /** Method to retrieve an argument by its name.
+     * @param name The name of the argument as a <strong>String</strong>.
+     * @return The <strong>Argument</strong> requested, or <i>null</i> if no such argument.
+     */
+    public Argument getArgument(String name) {
+	// The name given may still include the '-'
+	if(name.startsWith("-")) {
+	    name = name.substring(1);
+	}
+	ArrayList arguments = getArguments(true, true);
+	for(int i = 0; i < arguments.size(); i++) {
+	    Argument argument = (Argument)arguments.get(i);
+	    if(argument.getName().equals(name)) {
+		return argument;
+	    }
+	}
+	return null;
+    }
+
+     /** Retrieve all of the arguments available to this base download, including its super downloads arguments. Some complexity is added by allowing the caller to choose whether they want normal arguments, custom arguments, or both.
+      * @return an ArrayList of all of the arguments, starting with those for this download and ending with the arguments for basplug or similiar root download
+      */
+    public ArrayList getArguments(boolean include_normal, boolean include_custom) {
+	ArrayList arguments = new ArrayList();
+	if(include_normal && include_custom) {
+	    arguments.addAll(this);
+	}
+	else {
+	    int size = size();
+	    for(int i = 0; i < size; i++) {
+		Argument argument = (Argument) get(i);
+		if(argument.isCustomArgument()) {
+		    if(include_custom && !arguments.contains(argument)) {
+			arguments.add(argument);
+		    }
+		}
+		else {
+		    if(include_normal && !arguments.contains(argument)) {
+			arguments.add(argument);
+		    }
+		}
+		argument = null;
+	    }
+	}
+	if(super_download != null) {
+	    ArrayList remainder = super_download.getArguments(include_normal, include_custom);
+	    remainder.removeAll(arguments);
+	    arguments.addAll(remainder);
+	}
+	return arguments;
+    }
+
+    public String getDescription() {
+	return description;
+    }
+
+    public Element getElement() {
+	return element;
+    }
+
+    /** Method to retrieve a download name.
+     * @return A <strong>String</strong> containing the downloads name.
+     */
+    public String getName() {
+	if(name == null && element != null) {
+	    name = element.getAttribute(StaticStrings.TYPE_ATTRIBUTE);
+	}
+	return name;
+    }
+
+    public boolean isAbstract() {
+	return is_abstract;
+    }
+
+    public boolean isAssigned() {	
+	return (element != null && !element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.FALSE_STR));
+    }
+
+    public void setAssigned(boolean assigned) {
+	if(element != null) {
+	    element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
+	}
+    }
+
+
+    /** Method to set the value of desc.
+     * @param description The new value of desc as a <strong>String</strong>.
+     */
+    public void setDescription(String description) {
+	this.description = description;
+    }
+
+    public void setElement(Element element) {
+	this.element = element;
+    }
+
+    public void setIsAbstract(boolean is_abstract) {
+	this.is_abstract = is_abstract;
+    }
+
+    /** Method to set the value of name.
+     * @param name The new value of name as a <strong>String</strong>.
+     */
+    public void setName(String name) {
+	this.name = name;
+    }
+
+    /** Method to set the value of the super_download.
+     * @param super_download The new value of super_download as a <strong>Download</strong>, or <i>null</i> if this class has no inheritance.
+     */
+    public void setSuper(Download super_download) {
+	this.super_download = super_download;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/DownloadJob.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/DownloadJob.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/DownloadJob.java	(revision 31635)
@@ -0,0 +1,656 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.download;
+
+import java.awt.event.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.GAuthenticator;
+import org.greenstone.gatherer.greenstone.LocalGreenstone;
+import org.greenstone.gatherer.file.WorkspaceTree;
+import org.greenstone.gatherer.util.AppendLineOnlyFileDocument;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.cdm.Argument;
+import org.greenstone.gatherer.collection.*;
+/**
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.0
+ */
+public class DownloadJob
+    implements ActionListener {
+	 
+    private boolean debug;
+    private boolean higher_directories;
+    private boolean no_parents;
+    private boolean other_hosts; 
+    private boolean page_requisites; 
+    private boolean quiet;
+
+    private AppendLineOnlyFileDocument download_log;
+
+    private DownloadProgressBar progress;
+
+    private int depth;
+    private int previous_state;
+    private int state;
+
+    private String download_url = "";
+    
+    //    private String current_url;
+    //    private String destination;
+    private String proxy_pass;
+    private String proxy_user;
+
+    private Vector encountered_urls;
+    private Vector failed_urls;
+    private Download download;
+    private DownloadScrollPane mummy;
+    private HashMap download_option;
+
+    public static int COMPLETE = 0;
+    public static int PAUSED   = 1;
+    public static int RUNNING  = 2;
+    public static int STOPPED  = 3;
+
+    public static int UNKNOWN_MAX  = 0;
+    public static int DEFINED_MAX  = 1;
+    public static int UNDEFINED_MAX  = 2;
+    
+    // To prematurely terminate wget, we will need to use sockets and find a free port.
+    // We will look at a limited range of ports. This range will be reused (circular buffer)
+    private static final int PORT_BASE = 50000;
+    private static final int PORT_BLOCK_SIZE = 100;   
+    private static int nextFreePort = PORT_BASE;  // Keep track what port numbers we have checked for availability
+    int port;                                     // package access. The socket port number this instance of DownloadJob will use
+    
+    private String mode = null;
+   
+    private String proxy_url;
+    
+    /**
+     */
+    public DownloadJob(Download download, String proxy_pass, String proxy_user, DownloadScrollPane mummy, String mode, String proxy_url) {
+	URL url = null;
+	int folder_hash;
+
+        this.proxy_url =  proxy_url;
+	
+       	download_option = downloadToHashMap(download);
+	if (!mode.equals("Z3950") && !mode.equals("SRW")) {
+	    Argument url_arg = (Argument)download_option.get((String)"url");
+	    download_url =  url_arg.getValue();
+	    
+	}
+	else {
+	    Argument host_arg = (Argument)download_option.get((String)"host");
+	    Argument port_arg = (Argument)download_option.get((String)"port");
+	    download_url = host_arg.getValue() + ":" +port_arg.getValue();
+	}
+       
+	folder_hash = download_url.hashCode();
+	String log_filename = Utility.getLogDir(null) + "download-"+ mode + folder_hash + ".log";
+	File log_file = new File(log_filename);
+	if(log_file.exists()) {
+	    log_file.delete();
+	}
+
+	File parent_log_file = log_file.getParentFile();
+	parent_log_file.mkdirs();
+	parent_log_file = null;
+	log_file = null;
+
+	this.download_log = new AppendLineOnlyFileDocument(log_filename, false);
+   
+	this.proxy_pass = proxy_pass;
+	this.proxy_user = proxy_user;
+	this.mummy = mummy;
+	this.mode = mode;
+	this.download = download;
+
+	progress = new DownloadProgressBar(this,download_url, true);
+	encountered_urls = new Vector();
+	failed_urls = new Vector();
+
+	previous_state = STOPPED;
+	state = STOPPED;
+    }
+
+    private HashMap downloadToHashMap(Download download)
+    {
+	HashMap download_option = new HashMap();
+	ArrayList arguments = download.getArguments(true, false);
+	for(int i = 0; i < arguments.size(); i++) {
+	    Argument argument = (Argument) arguments.get(i);
+	    download_option.put(argument.getName(), argument);
+	}
+	return download_option;
+    }
+
+    /** Depending on which button on the progress bar was pushed,
+     * this method will affect the state of the DownloadJob and perhaps make
+     * calls to wget.class if necessary.
+     * @param event The ActionEvent fired from within the DownloadProgressBar
+     * which we must respond to.
+     */
+    public void actionPerformed(ActionEvent event) {
+	// The stop_start_button is used to alternately start or stop the
+	// job. If the current state of the job is paused then this
+	// restart is logically equivalent to a resume.
+	if(event.getSource() == progress.stop_start_button) {
+	    previous_state = state;
+	    if (state == RUNNING) {
+		state = STOPPED;
+	    } else {
+		//previous_state = state;
+		state = RUNNING;
+		mummy.resumeThread();
+	    }
+	}
+	else if (event.getSource() == progress.close_button) {
+	    if(state == RUNNING) {
+		previous_state = state;
+		state = STOPPED; // do we need to do anything else to stop this?
+	    } 
+	    mummy.deleteDownloadJob(this);
+	}
+    }
+
+    /** Given a portnumber to check, returns true if it is available
+     * (if nothing's listening there already). */
+    public static boolean isPortAvailable(int portnum) {
+	Socket tmpSocket = null;
+	try {
+	    tmpSocket = new Socket("localhost", portnum);
+	    tmpSocket.close();
+	    return false;
+
+	} catch(ConnectException ex){
+	    // "Signals that an error occurred while attempting to connect a socket 
+	    // to a remote address and port. Typically, the connection was refused
+	    // remotely (e.g., no process is listening on the remote address/port)."
+	    System.err.println("Port " + portnum + " not yet in use.");
+	    tmpSocket = null;
+	    return true;
+
+	} catch(Exception ex) { 
+	    // includes BindException "Signals that an error occurred while attempting
+	    // to bind a socket to a local address and port. Typically, the port is in
+	    // use, or the requested local address could not be assigned."
+	    tmpSocket = null;
+	    return false;
+	}
+    }
+
+    /** Circular buffer. Modifies the value of nextFreePort (the buffer index). */
+    private void incrementNextFreePort() {
+	int offset = nextFreePort - PORT_BASE;
+	offset = (offset + 1) % PORT_BLOCK_SIZE;
+	nextFreePort = PORT_BASE + offset;
+    } 
+
+    public void callDownload() {
+
+	ArrayList command_list = new ArrayList();
+	
+	// the following also works for client-gli if downloading is enabled (when there's a gs2build directory inside gli)
+	command_list.add(Configuration.perl_path);
+	command_list.add("-S");
+	command_list.add(LocalGreenstone.getBinScriptDirectoryPath()+"downloadfrom.pl");
+	command_list.add("-download_mode");
+	command_list.add(mode);
+	command_list.add("-cache_dir");
+	command_list.add(Gatherer.getGLIUserCacheDirectoryPath());
+	// For the purposes of prematurely terminating wget from GLI (which creates a socket
+	// as a communication channel between GLI and Perl), it is important to tell the script
+	// that we're running as GLI. Because when running from the command prompt, it should
+	// not create this socket and do the related processing.
+	command_list.add("-gli");
+	
+	ArrayList all_arg = download.getArguments(true,false);
+	for(int i = 0; i < all_arg.size(); i++) {
+	    Argument argument = (Argument) all_arg.get(i);
+	    if(argument.isAssigned()) {
+		command_list.add("-" + argument.getName());
+		if(argument.getType() != Argument.FLAG) {
+		    command_list.add(argument.getValue());
+		}
+	    }   
+	}
+
+	String [] cmd = (String []) command_list.toArray(new String[0]);
+	DebugStream.println("Download job, "+command_list);
+
+	if (previous_state == DownloadJob.COMPLETE) {
+	    progress.mirrorBegun(true, true);
+	}
+	else {
+	    progress.mirrorBegun(false, true);
+	}
+
+	try {
+	    Runtime rt = Runtime.getRuntime();
+
+	    String [] env = null;
+	 
+	    Process prcs = null; 
+
+         
+	    if (Utility.isWindows()) {
+              	prcs = rt.exec(cmd);   
+	    }
+	    else {
+		if (proxy_url != null && !proxy_url.equals("")) {          	
+		    // Specify proxies as environment variables
+		    // Need to manually specify GSDLHOME and GSDLOS also
+		    env = new String[4];
+                    proxy_url = proxy_url.replaceAll("http://","");
+		    env[0] = "http_proxy=http://"+proxy_url;
+		    env[1] = "ftp_proxy=ftp://"+proxy_url;
+		    env[2] = "GSDLHOME=" + Configuration.gsdl_path;
+		    env[3] = "GSDLOS=" + Gatherer.client_operating_system;
+		    prcs = rt.exec(cmd, env); 
+		} 
+		else if(Gatherer.isGsdlRemote && Gatherer.isDownloadEnabled) {
+		    // Not Windows, but running client with download panel
+		    // Need to manually specify GSDLHOME and GSDLOS
+		    env = new String[2];
+		    env[0] = "GSDLHOME=" + Configuration.gsdl_path;
+		    env[1] = "GSDLOS=" + Gatherer.client_operating_system;
+		    prcs = rt.exec(cmd, env); 
+		}
+		else {
+		    // Will inherit the GLI's environment, with GSDLHOME and GSDLOS set
+		    prcs = rt.exec(cmd);
+		}
+	    }
+	    //System.out.println(newcmd);	    
+
+	    // Can use the following if debugging WgetDownload.pm - Reads debug stmts from the perl process' STDIN stream
+	    //(new PerlReaderThread(prcs)).start();
+
+	    InputStream is = prcs.getInputStream();
+	    BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+
+	    // To be able to stop Wget, we use sockets to communicate with the perl process that launched wget
+	    if (mode.equals("Web") || mode.equals("MediaWiki")) { // wget download modes other than OAI
+		
+		// Need to find an available (unused) port within the range we're looking for to pass it
+		// the Perl child process, so that it may set up a listening ServerSocket at that port number
+		try {
+		    boolean foundFreePort = false;
+		    for(int i = 0; i < PORT_BLOCK_SIZE; i++) {
+
+			if(isPortAvailable(nextFreePort)) {
+			    foundFreePort = true;
+			    break;
+			    
+			} else {
+			    incrementNextFreePort();
+			}
+		    }
+		    
+		    if(foundFreePort) {
+			// Free port number currently found becomes the port number of the socket that this
+			// DownloadJob instance will be connecting to when the user wants to prematurely stop Wget.
+			this.port = nextFreePort;
+			incrementNextFreePort();
+			
+		    } else {
+			throw new Exception("Cannot find an available port in the range " 
+					    + PORT_BASE + "-" + (PORT_BASE+PORT_BLOCK_SIZE)
+					    + "\nwhich is necessary for forcibly terminating wget.");
+		    }
+
+		    // Communicate the chosen port for this DownloadJob instance to the perl process, so
+		    // that it can set up a ServerSocket at that port to listen for any signal to terminate wget
+		    OutputStream os = prcs.getOutputStream();
+		    String p = ""+this.port+"\n";
+		    System.err.println("Portnumber found: " + p);
+
+		    os.write(p.getBytes());
+		    os.close();
+		  
+		} catch(Exception ex) {
+		    System.err.println("Sent available portnumber " + this.port + " to process' outputstream.\nBut got exception: " + ex);
+		}
+	    }
+	    
+	    BufferedReader br = new BufferedReader(new InputStreamReader(prcs.getErrorStream()));
+	    // Capture the standard error stream and search for two particular occurrences.
+	    String line="";
+	    boolean ignore_for_robots = false;
+	    int max_download = DownloadJob.UNKNOWN_MAX;
+	   
+	    while ((line = br.readLine()) != null && !line.trim().equals("<<Finished>>") && state != STOPPED) {
+		if ( max_download == DownloadJob.UNKNOWN_MAX) {
+		    if(line.lastIndexOf("<<Defined Maximum>>") != -1) {
+			max_download = DownloadJob.DEFINED_MAX;
+		    }
+		    else if (line.lastIndexOf("<<Undefined Maximum>>") != -1) {
+			max_download = DownloadJob.UNDEFINED_MAX;
+		    }
+		}
+		else if(max_download == DownloadJob.UNDEFINED_MAX) {
+		    DebugStream.println(line);
+		    download_log.appendLine(line);
+		    // The first magic special test is to see if we've just
+		    // asked for the robots.txt file. If so we ignore
+		    // the next add and then the next complete/error.
+		    if(line.lastIndexOf("robots.txt;") != -1) {
+			DebugStream.println("***** Requesting robot.txt");
+			ignore_for_robots = true;
+		    }
+		    // If line contains "=> `" display text as the 
+		    // currently downloading url. Unique to add download.
+		    else if(line.lastIndexOf("=> `") != -1) {
+			if(!ignore_for_robots) {
+			    // Add download
+			    String new_url = line.substring(line.indexOf("`") + 1, line.lastIndexOf("'"));
+			    progress.addDownload("file"); //addDownload("http:/" + new_url.substring(cachedir_prefix_length()-1));				
+			}
+		    }
+		    // If line contains "/s) - `" set currently 
+		    // downloading url to "Download Complete".
+		    else if(line.lastIndexOf("/s) - `") != -1) {
+			String current_file_downloading = line.substring(line.indexOf("`") + 1, line.lastIndexOf("'"));
+			if(!ignore_for_robots) {
+			    DebugStream.println("Not ignore for robots");
+			    // Download complete
+			    downloadComplete(current_file_downloading);
+			}
+			else {
+			    DebugStream.println("Ignore for robots");
+			    ignore_for_robots = false;
+			}
+		    }
+		    // The already there line begins "File `..." However this
+		    // is only true in english, so instead I looked and there
+		    // are few (if any at all) other messages than those above
+		    // and not overwriting messages that use " `" so we'll 
+		    // look for that. Note this method is not guarenteed to be
+		    // unique like the previous two.
+		    else if(line.lastIndexOf(" `") != -1) {
+			// Not Overwriting
+			DebugStream.println("Already there.");
+			String new_url = 
+			    line.substring(line.indexOf("`") + 1, line.lastIndexOf("'"));
+			progress.addDownload("file"); //addDownload("http:/" + new_url.substring(cachedir_prefix_length()-1));
+			downloadWarning();
+		    }
+		    // Any other important message starts with the time in the form hh:mm:ss
+		    else if(line.length() > 7) {
+			if(line.charAt(2) == ':' && line.charAt(5) == ':') {
+			    if(!ignore_for_robots) {
+				DebugStream.println("Error.");
+				downloadFailed();
+			    }
+			    else {
+				ignore_for_robots = false;
+			    }
+			}
+		    }
+		}
+		else if (max_download == DownloadJob.DEFINED_MAX) {
+		    if (line.lastIndexOf("<<Total number of record(s):") != -1) {
+			String total_ID = line.substring(line.indexOf(":") + 1, line.indexOf(">"));
+			progress.setTotalDownload((Integer.valueOf(total_ID)).intValue());
+			progress.resetFileCount();	
+			progress.addDownload("files"); // for display: "Downloading files"
+		    }
+		    else if (line.lastIndexOf("<<Done>>") != -1) {
+			progress.increaseFileCount();
+		    }
+		    else if(line.lastIndexOf("<<Done:") != -1) {
+			String completed_amount = line.substring(line.indexOf(":") + 1, line.indexOf(">"));
+			progress.increaseFileCount((Integer.valueOf(completed_amount)).intValue());
+		    }
+		    
+		    DebugStream.println(line);
+		    download_log.appendLine(line);
+		}
+		else {
+		    System.out.println("Error!!");
+		    System.exit(-1);
+		}
+	    }
+
+	    if(state == STOPPED) {
+		boolean terminatePerlScript = true;
+			
+		// When GLI is working with wget-based download modes other than OAI (MediaWiki and Web 
+		// download) and the STOP button has been pressed, wget needs to be prematurely terminated.
+		// Only wget download modes Web and MediaWiki require the use of sockets to communicate
+		// with the perl script in order to get wget to terminate. Other download modes, including
+		// wgetdownload mode OAI, can terminate in the traditional manner: close process inputstream 
+		// and kill perl process. OAI launches many wgets. So that when the perl process is terminated,
+		// the currently running wget will finish off but other wgets are no longer launched. 
+		if(prcs != null && (mode.equals("Web") || mode.equals("MediaWiki"))) { 
+
+		    // create a socket to the perl child process and communicate the STOP message
+		    Socket clientSocket = null;
+		    if(clientSocket == null) {
+			try {
+			    clientSocket = new Socket("localhost", this.port); // connect to the port chosen for this DownloadJob instance
+			    
+			    BufferedReader clientReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
+			    String response = clientReader.readLine(); // see if we've been connected
+			    System.err.println("Communicating with perl download script on port " + this.port 
+					       + "\nGot response from perl: " + response); 
+			    
+			    // Send the STOP signal
+			    OutputStream os = clientSocket.getOutputStream();
+			    String message = "<<STOP>>\n";
+			    os.write(message.getBytes());
+			    response = clientReader.readLine(); // see whether the stop signal has been received
+			    System.err.println("GLI sent STOP signal to perl to terminate wget." 
+					       + "\nGot response from perl: " + response);
+
+			    response = clientReader.readLine(); // see whether the perl script is ready to be terminated
+			    System.err.println("Got another response from perl: " + response);
+			    os.close();
+			    
+			    clientReader.close();
+			    clientSocket.close(); // close the clientSocket (the Perl end will close the server socket that Perl opened)
+			    clientReader = null;
+			    clientSocket = null;
+			    
+			    if(response == null) {
+				terminatePerlScript = false;
+			    }
+			} catch(IOException ex) {
+			    System.err.println("Tried to communicate through client socket - port " + this.port + ", but got exception: " + ex);
+			} catch(Exception ex) {
+			    System.err.println("Tried to open client socket, but got exception: " + ex);
+			}
+		    }
+		}
+		
+		//prcs.getInputStream().close();
+		prcs.getErrorStream().close();
+		br.close();
+		br = null;
+		if(terminatePerlScript) {
+		    prcs.destroy(); // This doesn't always work, but it's worth a try		
+		    prcs = null;
+		}
+
+		// Notify the DownloadScrollPane which is waiting on this job to complete that we are ready 
+		synchronized(this) {
+		    this.notify();
+		}
+	    }
+	}
+	catch (Exception ioe) {
+	    //message(Utility.ERROR, ioe.toString());
+	    //JTest
+	    DebugStream.printStackTrace(ioe);
+	}
+	// If we've got to here and the state isn't STOPPED then the
+	// job is complete.
+	if(state == DownloadJob.RUNNING) {
+	    progress.mirrorComplete();
+	    previous_state = state;
+	    state = DownloadJob.COMPLETE;
+	}
+	// refresh the workspace tree
+ 	Gatherer.g_man.refreshWorkspaceTree(WorkspaceTree.DOWNLOADED_FILES_CHANGED);
+    }
+
+
+    /** Called by the WGet native code when the current download is 
+     * completed. In turn all download listeners are informed.
+     */
+    public void downloadComplete() {
+	progress.downloadComplete();
+    }
+
+
+    public void downloadComplete(String current_file_downloading)
+    {
+	progress.downloadComplete();
+	DebugStream.println("Download complete: " + current_file_downloading);
+    }
+
+
+    /** Called by the WGet native code when the requested download returns
+     * a status code other than 200.
+     */
+    public void downloadFailed() {
+	// TODO!!
+	//failed_urls.add(current_url); // It is the current url that failed
+	progress.downloadFailed();
+	//DebugStream.println("Download failed: " + current_url);
+    } 
+	 
+    /**
+     */
+    public void downloadWarning() {
+	progress.downloadWarning();
+    }
+
+    public AppendLineOnlyFileDocument getLogDocument() {
+	return download_log;
+    }
+
+    /**
+     * @return Returns the progress bar associated with this job.
+     */
+    public DownloadProgressBar getProgressBar() {
+	return progress;
+    }
+
+    /** Called to discover if the user wanted this thread to run or if
+     * it is paused.
+     * @return An int representing the current DownloadJob state.
+     */
+    public int getState() {
+	return state;
+    }
+
+    /** Returns the current state of the stop flag for this job.
+     * @return A boolean representing whether the user has requested to
+     * stop.
+     */
+    public boolean hasSignalledStop() {
+	if(state == DownloadJob.STOPPED || state == DownloadJob.PAUSED || 
+	   state == DownloadJob.COMPLETE) {
+	    return true;
+	}
+	return false;
+    }
+
+    public void setState(int state) {
+	previous_state = this.state;
+	this.state = state;
+    }
+
+    /** A convenience call.
+     * @return A String representing the url of the initial url (root node of the mirrored tree).
+     */
+    public String toString() {
+	return download_url;
+    }
+
+    /** Called by the WGet native code to signal the current progress of 
+     * downloading.
+     * @param current A long representing the number of bytes that have 
+     * been downloaded since the last update. 
+     * @param expected A long representing the total number of bytes
+     * expected for this download.
+     */
+    public void updateProgress(long current, long expected) {
+	progress.updateProgress(current, expected);
+    }
+
+    
+    // Inner thread class that reads from process downloadfrom.pl's errorstream
+    private class PerlReaderThread extends Thread {
+	Process prcs = null;
+
+	public PerlReaderThread(Process proc) {
+	    this.prcs = proc;
+	}
+
+	public void run() {
+	    try {
+		if(prcs != null) {
+		    String message = null;
+		    BufferedReader eReader = new BufferedReader(new InputStreamReader(prcs.getInputStream()));
+		    while(prcs != null && (message = eReader.readLine()) != null) {
+			if(!message.equals("\n")) {
+			    System.err.println("**** Perl STDOUT: " + message);
+			}
+		    }
+		    
+		    if(prcs != null && eReader != null) {
+			eReader.close();
+			eReader = null;
+			System.err.println("**** Perl ENDed.");
+		    }
+		}
+	    } catch(Exception e) {
+		System.err.println("Thread - caught exception: " + e);
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/DownloadProgressBar.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/DownloadProgressBar.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/DownloadProgressBar.java	(revision 31635)
@@ -0,0 +1,423 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.download;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+//import org.greenstone.gatherer.collection.DownloadJob;
+import org.greenstone.gatherer.util.AppendLineOnlyFileDocument;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.gui.*;
+
+public class DownloadProgressBar
+    extends JPanel 
+    implements ActionListener {
+
+    private boolean simple = false;
+
+    private Dimension bar_size = new Dimension(520, 25);
+
+    private Dimension MINIMUM_BUTTON_SIZE = new Dimension(100, 25);
+    private  Dimension PROGRESS_BAR_SIZE = new Dimension(580,75);
+    
+    private int current_action;
+    private int err_count;
+    private int file_count;
+    private int total_count;
+    private int warning_count;
+
+    private JLabel current_status;
+    private JLabel main_status;
+    private JLabel results_status;
+	 
+    private JPanel center_pane;
+    private JPanel inner_pane;
+
+    private JProgressBar progress;
+
+    private long file_size;
+    private long total_size;
+
+    private String current_url;
+    private String initial_url;
+
+    private DownloadJob owner;
+
+    public JButton stop_start_button;
+    public JButton log_button;
+    public JButton close_button;
+
+    public DownloadProgressBar(DownloadJob owner, String initial_url, boolean simple) {
+	this.owner = owner;
+	this.current_url = null;
+	this.err_count = 0;
+	this.initial_url = initial_url;
+	this.file_count = 0;
+	this.file_size = 0;
+	this.simple = simple;
+	this.total_count = 0;
+	this.total_size = 0;
+	this.warning_count = 0;
+
+	this.setLayout(new BorderLayout());
+	this.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
+
+	this.current_action = DownloadJob.STOPPED;
+
+	inner_pane = new JPanel(new BorderLayout());
+	inner_pane.setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
+
+	center_pane = new JPanel(new GridLayout(3,1));
+
+	main_status = new JLabel();
+
+	current_status = new JLabel();
+		  
+	results_status = new JLabel();
+
+	progress = new JProgressBar();
+	progress.setStringPainted(true);
+	progress.setMinimum(0);
+	progress.setMaximum(0);
+	progress.setEnabled(false);
+	progress.setString(Dictionary.get("Mirroring.DownloadJob.Waiting"));
+	inner_pane.add(progress, BorderLayout.CENTER);
+
+	center_pane.add(main_status);
+	center_pane.add(current_status);
+	center_pane.add(results_status);
+
+	JPanel button_pane = new JPanel();
+
+	stop_start_button = new GLIButton(Dictionary.get("Mirroring.DownloadJob.Pause"),Dictionary.get("Mirroring.DownloadJob.Pause_Tooltip"));
+	stop_start_button.addActionListener(this);
+	stop_start_button.addActionListener(owner);
+	stop_start_button.setMinimumSize(MINIMUM_BUTTON_SIZE);
+	stop_start_button.setMnemonic(KeyEvent.VK_P);
+	stop_start_button.setEnabled(true);
+	
+
+	log_button = new GLIButton(Dictionary.get("Mirroring.DownloadJob.Log"),Dictionary.get("Mirroring.DownloadJob.Log_Tooltip"));
+	log_button.addActionListener(owner);
+	log_button.addActionListener(this);
+	log_button.setEnabled(false);
+	log_button.setMinimumSize(MINIMUM_BUTTON_SIZE);
+	log_button.setMnemonic(KeyEvent.VK_L);
+
+
+	close_button =  new GLIButton(Dictionary.get("Mirroring.DownloadJob.Close"),Dictionary.get("Mirroring.DownloadJob.Close_Tooltip"));
+	close_button.addActionListener(owner);
+	close_button.addActionListener(this);
+	close_button.setMinimumSize(MINIMUM_BUTTON_SIZE);
+	close_button.setMnemonic(KeyEvent.VK_C);
+	close_button.setEnabled(true);
+
+
+	// Layout - or at least some of it
+
+	inner_pane.add(center_pane, BorderLayout.NORTH);
+
+	button_pane.setLayout(new GridLayout(3,1));
+	button_pane.add(stop_start_button);
+	button_pane.add(log_button);
+	button_pane.add(close_button);
+
+	this.add(inner_pane, BorderLayout.CENTER);
+	this.add(button_pane, BorderLayout.EAST);
+
+	// Make the labels, etc update.
+	refresh();
+    }
+
+    public void actionPerformed(ActionEvent event) {
+	Object source = event.getSource();
+	if(source == stop_start_button) {
+	    // If we are running, stop.
+	    if (current_action == DownloadJob.RUNNING) {
+		current_action = DownloadJob.STOPPED;
+                stop_start_button.setText(Dictionary.get("Mirroring.DownloadJob.Resume"));
+                stop_start_button.setToolTipText(Dictionary.get("Mirroring.DownloadJob.Resume_Tooltip"));
+	        
+		progress.setString(Dictionary.get("Mirroring.DownloadJob.Download_Stopped"));
+		progress.setIndeterminate(false);
+	    } else {
+		current_action = DownloadJob.RUNNING;
+		// we are stopped, so restart
+                stop_start_button.setText(Dictionary.get("Mirroring.DownloadJob.Pause"));
+                stop_start_button.setToolTipText(Dictionary.get("Mirroring.DownloadJob.Pause_Tooltip"));
+		progress.setString(Dictionary.get("Mirroring.DownloadJob.Download_Progress"));
+		progress.setIndeterminate(true);
+	    }
+	}
+	else if(source == close_button) {
+	    // If we are running, stop.
+	    if (current_action == DownloadJob.RUNNING) {
+		current_action = DownloadJob.STOPPED;
+             	progress.setString(Dictionary.get("Mirroring.DownloadJob.Download_Stopped"));
+	
+		progress.setIndeterminate(false);
+	    }
+	}
+	else if(source == log_button) {
+           
+	    LogDialog dialog = new LogDialog(owner.getLogDocument());
+	    dialog.setVisible(true);
+	    dialog = null;
+	}
+
+	refresh();
+    }
+
+    /** This method is called when a new download is begun. The
+     * details of the download are updated and a new JProgressBar
+     * assigned to track the download.
+     * @param url The url String of the file that is being downloaded.
+     */
+    public void addDownload(String url) {
+	current_url = url;
+	file_size = 0;
+	refresh();
+    }
+
+    /** When the download of the current url is completed, this method
+     * is called to enlighten the DownloadProgressBar of this fact.
+     */
+    public void downloadComplete() {
+	current_url = null;
+	file_count++;
+	if(total_count < (file_count + err_count + warning_count)) {
+	    total_count = (file_count + err_count + warning_count);
+	}
+	progress.setValue(progress.getMaximum());
+	//Dictionary.setText(progress, "Mirroring.DownloadJob.Download_Complete");
+	refresh();
+    }
+
+    public void downloadFailed() {
+	err_count++;
+	if(total_count < (file_count + err_count + warning_count)) {
+	    total_count = (file_count + err_count + warning_count);
+	}
+	refresh();
+    }
+
+    public void downloadWarning() {
+	warning_count++;
+	if(total_count < (file_count + err_count + warning_count)) {
+	    total_count = (file_count + err_count + warning_count);
+	}
+	refresh();
+    }
+
+    public void setTotalDownload(int total_download) {
+	total_count = total_download;
+	refresh();
+    }
+
+    public Dimension getPreferredSize() {
+	return PROGRESS_BAR_SIZE;
+    }
+
+    public void increaseFileCount() {
+	file_count++;
+	refresh();
+    }
+
+    public void increaseFileCount(int amount) {
+	file_count += amount;
+	refresh();
+    }
+
+    public void resetFileCount() {
+	file_count = 0;
+	refresh();
+    }
+
+    /** When a mirroring task is first initiated this function is called
+     * to set initial values for the variables if necessary and to 
+     * fiddle visual components such as the tool tip etc.
+     * @param reset A Boolean specifying whether the variables should be
+     * reset to zero.
+     */
+    public void mirrorBegun(boolean reset, boolean simple) {
+	if(reset) {
+	    this.file_count = 0;
+	    this.file_size = 0;
+	    this.total_count = 0;
+	    this.total_size = 0;
+	    this.err_count = 0;
+	    this.warning_count = 0;
+	}
+	current_action = DownloadJob.RUNNING;
+	stop_start_button.setEnabled(true);
+	log_button.setEnabled(true);
+	if(simple) {
+	    progress.setIndeterminate(true);
+	    progress.setString(Dictionary.get("Mirroring.DownloadJob.Download_Progress"));
+	}
+    }	
+
+    /** Once a mirroring task is complete, if the DownloadJob returns from the
+     * native call but the status is still running, then this method
+     * is called to once again tinker with the pretty visual 
+     * components.
+     */
+    public void mirrorComplete() {
+	current_action = DownloadJob.COMPLETE;
+	current_url = null;
+	if(simple) {
+	    progress.setIndeterminate(false);
+	}
+	progress.setValue(progress.getMaximum());
+	progress.setString(Dictionary.get("Mirroring.DownloadJob.Download_Complete"));
+	current_status.setText(Dictionary.get("Mirroring.DownloadJob.Download_Complete")); 
+		// will display "Download Complete" once an OAI download session has finished running
+
+	stop_start_button.setEnabled(false);
+	this.updateUI();
+    }
+
+    /** When called this method updates the DownloadProgressBar to reflect
+     * the amount of the current file downloaded.
+     */
+    public void updateProgress(long current, long expected) {
+	file_size = file_size + current;
+	if(!progress.isIndeterminate()) {
+	    // If current is zero, then this is the 'precall' before the
+	    // downloading actually starts.
+	    if(current == 0) {
+		// Remove the old progress bar, then deallocate it.
+		inner_pane.remove(progress);
+		progress = null;
+		if(expected == 0) {
+		    // We don't have a content length. This bar will go from 0 to 100 only!
+		    progress = new JProgressBar(0, 100);
+		}
+		else {
+		    // Assign a new progress bar of length expected content length.
+		    progress = new JProgressBar(0, (new Long(expected)).intValue());
+		}
+		progress.setEnabled(true);
+		// Add the new progress bar.
+		inner_pane.add(progress, BorderLayout.CENTER);
+		inner_pane.updateUI();
+	    }
+	    // Otherwise if expected is not zero move the progress bar and
+	    // update percent complete.
+	    else if (expected != 0) {
+		progress.setValue((new Long(file_size)).intValue());
+		int p_c = (new Double(progress.getPercentComplete() * 100)).intValue();
+		progress.setString(p_c + "%");
+		progress.setStringPainted(true);
+	    }
+	    // Finally, in the case we have no content length, we'll instead
+	    // write the current number of bytes downloaded again.
+	    else {
+		progress.setString(file_size + " b");
+		progress.setStringPainted(true);
+	    }
+	}
+	refresh();
+    }
+
+    /** Causes the two labels associated with this DownloadProgressBar object to
+     * update, thus reflecting the progression of the download. This 
+     * method is called by any of the other public setter methods in this 
+     * class.
+     */
+    private void refresh() {
+	// Refresh the contents of main label.
+	String args1[] = new String[1];
+	args1[0] = initial_url.toString();
+	main_status.setText(Dictionary.get("Mirroring.DownloadJob.Downloading", args1));
+
+	if (current_url != null) {
+	    // Refresh the current label contents.
+	    String args2[] = new String[1];
+	    args2[0] = current_url;
+	    current_status.setText(Dictionary.get("Mirroring.DownloadJob.Downloading", args2));
+	}
+	else if (current_action == DownloadJob.STOPPED || current_action == DownloadJob.PAUSED) {
+	    current_status.setText(Dictionary.get("Mirroring.DownloadJob.Waiting_User"));
+	}
+	else {
+		current_status.setText(Dictionary.get("Mirroring.DownloadJob.Download_Complete"));
+	}
+
+	// Refresh the contents of results label
+	String args3[] = new String[4]; 
+	args3[0] = file_count + "";
+	args3[1] = total_count + "";
+	args3[2] = warning_count + "";
+	args3[3] = err_count + "";
+	results_status.setText(Dictionary.get("Mirroring.DownloadJob.Status", args3));
+
+	this.updateUI();
+    }
+
+    static final private Dimension DIALOG_SIZE = new Dimension(640,480);
+
+    private class LogDialog
+	extends JDialog {
+	
+	public LogDialog(AppendLineOnlyFileDocument document) {
+	    super(Gatherer.g_man, Dictionary.get("Mirroring.DownloadJob.Log_Title"));
+	    setSize(DIALOG_SIZE);
+	    JPanel content_pane = (JPanel) getContentPane();
+	    JTextArea text_area = new JTextArea(document);
+	    JButton button = new GLIButton(Dictionary.get("General.Close"));
+	    // Connection
+	    button.addActionListener(new CloseActionListener());
+	    // Layout
+	    content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    content_pane.setLayout(new BorderLayout());
+	    content_pane.add(new JScrollPane(text_area), BorderLayout.CENTER);
+	    content_pane.add(button, BorderLayout.SOUTH);
+	}
+
+	private class CloseActionListener
+	    implements ActionListener {
+	    public void actionPerformed(ActionEvent event) {
+		LogDialog.this.dispose();
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/DownloadScrollPane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/DownloadScrollPane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/DownloadScrollPane.java	(revision 31635)
@@ -0,0 +1,245 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.download;
+
+import java.awt.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.*;
+
+/** This class provides access to the functionality of the WGet program, either by calling it via a shell script or by the JNI. It maintains a queue of pending jobs, and the component for showing these tasks to the user.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class DownloadScrollPane
+    extends Thread {
+    
+    /** <i>true</i> if verbose debug messages should be displayed, <i>false</i> otherwise. */
+    private boolean debug = false;
+    /** <i>true</i> if successfully completed tasks should be automatically removed from the job queue. */
+    private boolean remove_complete_jobs = true;
+
+    private JPanel filler_pane = null;
+    /** The panel that the task list will be shown in. */
+    private JPanel list_pane;
+    /** The job currently underway. */
+    private DownloadJob job;
+    /** A scroll pane which will be used to display the list of pending tasks. */
+    private JScrollPane list_scroll;
+    /** A queue of download tasks. */
+    private Vector job_queue;
+    static final private boolean simple = true;
+
+        
+    public DownloadScrollPane() {
+	job = null;
+	job_queue = new Vector();
+	filler_pane = new JPanel();
+	list_pane = new JPanel();
+	list_pane.setLayout(new BoxLayout(list_pane, BoxLayout.Y_AXIS));
+	list_scroll = new JScrollPane(list_pane);
+    }
+
+    public void deleteDownloadJob(DownloadJob delete_me) {
+	if (delete_me == job) {
+	    try {
+		// Wait for the job to finish cleaning up, before we can continue cleaning up here.
+		// But sometimes the job has completed (terminated naturally) and in that case there's
+		// nothing to wait for.
+		synchronized(delete_me) {
+		    if (!delete_me.hasSignalledStop()) { // don't wait if DownloadJob.COMPLETED 
+			delete_me.wait();
+		    }
+		}
+	    } catch (Exception e) {
+		e.printStackTrace();
+	    }	    
+	}
+	// Close button pressed, get rid of this download's dedicated pane
+	finishedDownloadJob(delete_me, true);
+    }
+
+    /** To be called when a download job has terminated naturally or was prematurely stopped
+     *	via the close button. 
+     *  Gets rid of this download's pane with buttons and progress bar if prematurely stopped. */
+    protected void finishedDownloadJob(DownloadJob delete_me, boolean removeDisplay) {
+	if (delete_me.hasSignalledStop()) {
+	    if(removeDisplay) {
+		list_pane.remove(delete_me.getProgressBar());
+		list_pane.remove(filler_pane);
+	    }
+	    job_queue.remove(delete_me);
+	    if(job_queue.size() > 0) {
+		Dimension progress_bar_size = delete_me.getProgressBar().getPreferredSize();
+		Dimension list_pane_size = list_pane.getSize();
+		int height = list_pane_size.height - (job_queue.size() * progress_bar_size.height);
+		progress_bar_size = null;
+		if(height > 0) {
+		    filler_pane.setPreferredSize(new Dimension(list_pane_size.width, height));
+		    list_pane.add(filler_pane);
+		}
+		list_pane_size = null;
+	    }
+	    list_pane.updateUI();
+	}
+	else {
+	    DebugStream.println("Somehow we're trying to delete a job that is still running.");
+	}
+    }
+
+    public synchronized void downloadComplete() {
+	job.downloadComplete();
+    }
+
+    public synchronized void downloadFailed() {
+	job.downloadFailed();
+    }
+
+    public synchronized void downloadWarning() {
+	job.downloadWarning();
+    }
+
+    public JScrollPane getDownloadJobList() {
+	return list_scroll;
+    }
+
+    public synchronized boolean hasSignalledStop() {
+	return job.hasSignalledStop();
+    }
+
+    public void newDownloadJob(Download download, String mode, String proxy_url) {
+	// Create the job and fill in the details from gatherer.config.
+
+	DebugStream.println("About to create a new job");
+	
+        DownloadJob new_job = new DownloadJob(download, Configuration.proxy_pass, Configuration.proxy_user, this, mode, proxy_url);
+	// Tell it to run as soon as possible
+
+	new_job.setState(DownloadJob.RUNNING);
+	
+	// Add to job_queue job list.
+	job_queue.add(new_job);
+
+	// Now add it to the visual component, job list.
+
+	list_pane.remove(filler_pane);
+
+	Dimension progress_bar_size = new_job.getProgressBar().getPreferredSize();
+
+	Dimension list_pane_size = list_pane.getSize();
+
+	int height = list_pane_size.height - (job_queue.size() * progress_bar_size.height);
+
+	progress_bar_size = null;
+
+	list_pane.add(new_job.getProgressBar());
+
+	if(height > 0) {
+	    filler_pane.setPreferredSize(new Dimension(list_pane_size.width, height));
+	    list_pane.add(filler_pane);
+	}
+
+	list_pane_size = null;
+	//list_pane.setAlignmentX(Component.LEFT_ALIGNMENT);
+	list_pane.updateUI();
+	new_job = null;		    //job = (DownloadJob) job_queue.get(index);
+
+	synchronized(this) {
+	    notify(); // Just incase its sleeping.
+	}
+    }
+    
+    public synchronized void updateProgress(long current, long expected) {
+	job.updateProgress(current, expected);
+    }
+    
+    /* There may be times when the download thread is sleeping, but the
+     * user has indicated that a previously paused job should now begin
+     * again. The flag within the job will change, so we tell the thread
+     * to start again.
+     */
+    public void resumeThread() {
+	synchronized(this) {
+	    notify(); // Just incase its sleeping.
+	}
+    }
+    
+    public void run() {
+	while(true) {
+	    if(job_queue.size() > 0) {
+		while(!job_queue.isEmpty()) { 
+		    job = (DownloadJob) job_queue.firstElement(); 
+		    
+		    if(job.getState() == DownloadJob.RUNNING) {
+			DownloadJob delete_me = job;
+			String jobDisplayString = job.toString();
+			DebugStream.println("DownloadJob " + jobDisplayString + " Begun.");
+			System.err.println("DownloadJob " + job.port + " " + job.toString() + " Begun.");
+			job.callDownload();
+			// Job is done. Ended naturally, don't get rid of this download's separate display panel
+			finishedDownloadJob(delete_me, false); 
+			System.err.println("DownloadJob " + jobDisplayString + " complete.");
+			DebugStream.println("DownloadJob " + jobDisplayString + " complete."); // by this point job is null!
+			job = null;
+			delete_me = null;
+		    }
+		}
+		try {
+		    synchronized(this) {
+			DebugStream.println("WGet thread is waiting for DownloadJobs.");
+			wait();
+		    }
+		} catch (InterruptedException e) {
+		    // Time to get going again.
+		}
+	    }
+	    else {
+		try {
+		    synchronized(this) {
+			DebugStream.println("WGet thread is waiting for DownloadJobs.");
+			wait();
+		    }
+		} catch (InterruptedException e) {
+		    // Time to get going again.
+		}
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/ServerInfoDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/ServerInfoDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/download/ServerInfoDialog.java	(revision 31635)
@@ -0,0 +1,148 @@
+package org.greenstone.gatherer.download;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import javax.swing.event.*;
+import java.io.*;
+import java.util.ArrayList;
+import org.greenstone.gatherer.cdm.Argument;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.greenstone.LocalGreenstone;
+import org.greenstone.gatherer.gui.*;
+import org.greenstone.gatherer.util.Utility;
+
+public class ServerInfoDialog extends JDialog
+{    
+    static private Dimension SIZE = new Dimension(500, 300);
+    private String url;
+    private String mode;
+    private JPanel info_list_pane;
+    private JScrollPane info_scroll_pane;
+    private JDialog info_dialog;
+    private Download download;
+    private String proxy_url;
+    
+    public ServerInfoDialog(String url, String proxy_url, String mode, Download download) {
+	super();
+	this.url = url;
+	this.mode = mode;
+        this.download = download;
+        this.proxy_url = proxy_url;
+	this.info_dialog = this;
+	
+	this.setSize(SIZE);
+
+ 	JButton close_button = new GLIButton(Dictionary.get("General.Close"),Dictionary.get("General.Close_Tooltip"));
+ 	close_button.setEnabled(true); 
+	close_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    info_dialog.dispose();
+		}
+	    });
+	
+	JPanel button_pane = new JPanel();
+	button_pane.setLayout(new FlowLayout());
+	button_pane.setBorder(BorderFactory.createEtchedBorder());
+	button_pane.add(close_button);
+	
+	info_list_pane = new JPanel();
+	info_list_pane.setLayout(new BoxLayout(info_list_pane, BoxLayout.Y_AXIS));
+	info_list_pane.setBorder(BorderFactory.createEtchedBorder());
+	getServerInfo();
+
+	info_scroll_pane = new JScrollPane(info_list_pane);
+	
+	JPanel information_pane = new JPanel();
+	information_pane.setLayout(new BorderLayout());
+	information_pane.setBorder(BorderFactory.createEtchedBorder());
+	information_pane.add(info_scroll_pane,BorderLayout.CENTER);
+	
+	JPanel main_pane = new JPanel();
+	main_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	main_pane.setLayout(new BorderLayout());
+	main_pane.add(information_pane,BorderLayout.CENTER);
+	main_pane.add(button_pane,BorderLayout.SOUTH);
+
+	Container main_container = this.getContentPane();
+	main_container.add(main_pane);
+	
+	// Show
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(true);
+
+    }
+
+    private void getServerInfo()
+    {
+	ArrayList commands = new ArrayList();
+	commands.add(Configuration.perl_path);
+	commands.add("-S");
+	commands.add(LocalGreenstone.getBinScriptDirectoryPath()+"downloadfrom.pl");
+	commands.add("-info");
+	commands.add("-download_mode");
+	commands.add(mode);
+	
+       	ArrayList all_arg = download.getArguments(true,false);
+	for (int i = 0; i < all_arg.size(); i++) {
+	    Argument argument = (Argument) all_arg.get(i);
+	    
+	    if(argument.isAssigned()) {
+		commands.add("-" + argument.getName());
+		if(argument.getType() != Argument.FLAG) {
+		    commands.add(argument.getValue());
+		}
+	    }   
+	}
+	
+	String[] command = (String[]) commands.toArray(new String[0]);
+	DebugStream.println("Getting server info: "+commands);
+	try {
+	    String [] env = null;
+	    Process prcs = null; 
+	    Runtime rt = Runtime.getRuntime();
+	  
+	    if (Utility.isWindows()) {
+              	prcs = rt.exec(command);   
+	    }
+	    else {
+		if (proxy_url != null && !proxy_url.equals("")) {          	
+		    // Specify proxies as environment variables
+		    // Need to manually specify GSDLHOME and GSDLOS also
+		    env = new String[4];
+                    proxy_url = proxy_url.replaceAll("http://","");
+		    env[0] = "http_proxy=http://"+proxy_url;
+		    env[1] = "ftp_proxy=ftp://"+proxy_url;
+		    env[2] = "GSDLHOME=" + Configuration.gsdl_path;
+		    env[3] = "GSDLOS=" + Gatherer.client_operating_system;
+		    prcs = rt.exec(command, env); 
+		} 
+		else {
+		    // Will inherit the GLI's environment, with GSDLHOME and GSDLOS set
+		    prcs = rt.exec(command);
+		}
+	    }
+
+	    InputStreamReader isr = new InputStreamReader(prcs.getErrorStream());
+	    BufferedReader br = new BufferedReader(isr);
+	    String line;
+	    // Capture the standard error stream and seach for two particular occurences.
+	    while ((line = br.readLine()) != null && !line.trim().equals("<<Finished>>")) {
+	      
+		JLabel a_label = new JLabel(line);
+		a_label.setAlignmentX(Component.LEFT_ALIGNMENT);
+		info_list_pane.add(a_label);
+	    }
+	    
+	} catch (Exception ioe) {
+	    ioe.printStackTrace();
+	}
+	
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ActionRecorderDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ActionRecorderDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ActionRecorderDialog.java	(revision 31635)
@@ -0,0 +1,2135 @@
+package org.greenstone.gatherer.feedback;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.tree.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+import java.io.*;
+import java.awt.image.*;
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.text.MessageFormat;
+import javax.swing.border.*;
+import java.text.*;
+
+import org.greenstone.gatherer.util.JarTools;
+
+/**
+ * This is the special class listener. 
+ * <p>This class will record all the action did to any of the
+ * component inside the window that were open by the application. 
+ * <br> This listener will start listening since the beginning of the application 
+ * until the user quits from the application. It will record :
+ * <br>1. All the information about the window and its components where the action occured.
+ * <br>2. All the information about all windows and its components that were open when the action occured.
+ * <br>Note : point 1 and 2 only start when user wants to send feedback.
+ * <br>3. The action's command the user did.
+ * <br>4. The date of when the action occured.
+ * <br><br>The history of the action will be stored in history.log file, which then will be compressed
+ * to a zip file called MyZipFile.zip.</p>
+ * <p>The listener this class implements are only the listener interface that a javax.swing.* component
+ * could have as stated in API.Some of the listener however is not implemented here as it is considered the event
+ * listened by those listener are not essential to be recorded as they not making any major changes
+ * to the application.</p> 
+ * <p>This listener will only record maximum last 60 instructions user did to the application.</p>
+ * <p>In this class also we can add some stuff globally through out all the window open by
+ * the application.
+ * <br>This is the class will add a menu called Feedback through out all the window of type JFrame or JDialog
+ * open by the application.This menu will allow user to either sending feedback or viewing the history
+ * of all the action user did so far.
+ * @author Veronica Liesaputra
+ */
+public class ActionRecorderDialog
+    implements ActionListener,TreeModelListener,ItemListener,ListDataListener,ChangeListener,
+	       ListSelectionListener,MenuListener,TableColumnModelListener,TreeSelectionListener
+{
+    /**
+     * This hold the title of the window where the action occured.
+     */
+    private String myframe;
+
+    /**
+     * This hold the title of the window where user called send feedback.
+     */
+    private String feedbackframe;
+
+    /**
+     * This hold the instance of ComponentInformation to get all the information inside a window.
+     */
+    private ComponentInformation compInfo;
+    
+    /**
+     * This will hold a record of the action that user just did at the moment.
+     */
+    private History log;
+
+    /**
+     * This will reference to the window where the action occured.
+     */
+    private Window point;
+   
+    /**
+     * This hold the vector of all the History instances of all the record of action user did
+     * so far.
+     * This vector will hold the history of all the 30 actions user just did.
+     * There are 2 situation for this vector :
+     * <br>1. Before user choose to send feedback.
+     * <br>Here, If we want to add another record to the vector when the vector size already 30 then,
+     * all the 30 records inside the vector will be saved in the temp_history.log file. The vector will
+     * be cleared and then added with the new record.
+     * <br>If the user quits the application, then whats in the vector will be saved to history.log file.
+     * <br>2. After user choose to send feedback.
+     * <br>When user choose to send feedback,then what happen is what ever inside the vector at the moment
+     * will be saved to history.log file and then vector will be cleared and it will add the new instruction.
+     * <br> If we want to add another record to the vector when the vector size already 30 then,
+     * all the 30 records inside the vector will be saved in the temp_feedbackhist.log file. The vector will
+     * be cleared and then added with the new record.
+     * <br> If the user finished with the feedback then it will cleared all the vector and load the vector
+     * inside the history.log to it and add the new record to the vector.
+     * <br> If the user quits the application, then whats in the vector will not be saved.
+     */
+    private static Vector vector;
+
+    /**
+     * This is the flag to tell whether or not the image for the window where the action occured is
+     * finished being added properly to the appropriate record.
+     * If savefinish = true then it means that the thread of adding image to the record stop, otherwise
+     * it means its not yet finished so don't stop the thread.
+     */
+    private static boolean savefinish = true;
+
+    /**
+     * This is the thread that contains all the code that the application should be when the user exiting
+     * the application.
+     */
+    private Thread writeThread;
+
+    /**
+     * This is the thread that contains all the code to add the image of the window where the action
+     * occured to the proper record.
+     * This thread will stop if savefinish = true.
+     */
+    private Thread saveThread;
+
+    /**
+     * This is the runtime of this application.
+     */
+    private Runtime run;
+
+    /**
+     * This variable will hold the resource of the words that is stored in feedback.properties file.
+     * The calling using messages.getString(someString) will caused someString to be translated
+     * into some other string that is hold in that file.usually it will caused it to be translated
+     * to the language that the user use or choose to have as stated in Locale.
+     */
+    private static ResourceBundle messages;
+
+    /**
+     * This is an instance of ScreenShot that will allow this application to take screen shot of the whole
+     * screen or just the screen shot of the window where the actions occured.
+     */
+    private ScreenShot sh;
+
+    /**
+     * This HashMap stored the pair of model and its component.
+     * This will allow this listener to know which component the model where the actions occured belongs to.
+     */
+    private HashMap modelHash;
+
+    /**
+     * This is the flag variable to sign that the user start the Reporting Feedback sequence.
+     * If start_rec = false means that the user did not start reporting feedback sequence.
+     * Otherwise, it means the user start reporting feedback sequence, here there will be changes in the
+     * content of the vector used to save all the records.This also will change the appearance of the
+     * window that are currently open by the application. it will make toolbar value = true, rec = true and
+     * stop_rec = false.
+     */
+    private boolean start_rec = false;
+
+    /**
+     * This is the flag variable to sign that the user are now recording the Reporting Feedback sequence.
+     * If rec = true, it means its recording, otherwise its false.It also will decide
+     * the GUI appearance of the window that are currently open by the application.
+     */
+    private boolean rec = false;
+
+    /**
+     * This is the flag variable to sign that user are finished the Reporting Feedback sequence.
+     * If stop_rec = false, then it means user stop the Reporting Feedback sequence.So here, there will
+     * be changes in the vector's content. If rec = true then it will also delete the added toolbar on the currently open
+     * window by the application and its also will delete all the history and file taken when user doing the
+     * Reporting Feedback sequence.
+     */
+    private boolean stop_rec = true;
+
+    /**
+     * This is the thread do task to take history of the actions.
+     */
+    private Thread saveallThread;
+
+    /**
+     * This is the flag to say whether or not the send feedback button been pushed by user.
+     */
+    private boolean start_fd = false;
+
+    /**
+     * This is a blocker to say that user cannot do anything while we taking history.
+     */
+    private MouseListener mouse_blocker_listener = new MouseAdapter() {};
+
+    /**
+     * This is the feedback interface window.
+     */
+    private FeedbackInterface frame;
+
+    /**
+     * This is an instance of CompListener that will add listener to all the components inside it.
+     */
+    private CompListener compList;
+
+    /**
+     * This is the accelerator key (shortcuts key) for taking screenshot.
+     */
+    private KeyStroke shKey;
+
+    /**
+     * This is the accelerator key (shortcuts key) for reporting feedback.
+     */
+    private KeyStroke fdKey;
+
+    /**
+     * This is the accelerator key (shortcuts key) for viewing history.
+     */
+    private KeyStroke histKey;
+
+    /**
+     * This is the accelerator key (shortcuts key) for sending feedback straight away.
+     */
+    private KeyStroke sndfdKey;
+
+    /**
+     * This is the window that is active at the moment.
+     */
+    private Window activeWindow;
+
+    /**
+     * This is the modal window where user wants to take the screeshot or sending feedback.
+     */
+    private Window modalWindow;
+
+    /**
+     * This constructor will seeting up the data member to its appropriate value.
+     * It will get the vector that are stored in the history.log file and also it will
+     * setup the writeThread.
+     * @param currentLocale is the locale user choose all the text to be displayed as.
+     */
+    public ActionRecorderDialog (Locale currentLocale)
+    {
+	messages = ResourceBundle.getBundle("feedback",currentLocale);
+	setup_UIManager();
+
+	fdKey = KeyStroke.getKeyStroke("F2");
+	histKey = KeyStroke.getKeyStroke("F3");
+	shKey = KeyStroke.getKeyStroke("F5");
+	sndfdKey = KeyStroke.getKeyStroke("F12");
+	/*shKey = KeyStroke.getKeyStroke(KeyEvent.VK_SLASH,InputEvent.CTRL_MASK|
+	  InputEvent.SHIFT_MASK);*/
+
+	final ZipFile zip;
+	zip = new ZipFile();
+	zip.unZipFile();
+	vector = getPrev_log("history.log");
+	sh = new ScreenShot();
+	modelHash = new HashMap();
+	compInfo = new ComponentInformation(this,currentLocale,modelHash);
+        compList = new CompListener(this,modelHash);
+	EventQueue eq;
+	eq = Toolkit.getDefaultToolkit().getSystemEventQueue();
+	eq.push(new MyEventQueue());
+	run = Runtime.getRuntime();
+	Runnable writeRun;
+	writeRun = new Runnable()
+	    {
+		public void run()
+		{
+		    try
+			{
+			    if (stop_rec == true)
+				{
+				    File f;
+				    f = new File("history.log");
+				    FileOutputStream fos;
+				    fos = new FileOutputStream("history.log");
+				    ObjectOutputStream oos;
+				    oos = new ObjectOutputStream(fos);
+				    oos.writeObject(vector);
+				    vector.removeAllElements();
+				    vector = getPrev_log("temp_history.log");
+				    oos.writeObject(vector);
+				    oos.close();
+				    f = new File("temp_history.log");
+				    f.delete();
+				}
+
+			    File f2;
+			    f2 = new File("feedbackhist.log");
+			    f2.delete();
+			    f2 = new File("temp_feedbackhist.log");
+			    f2.delete();
+
+			    zip.sendZipFile();
+			    writeThread = null;
+			}
+		    catch (IOException exp) {}
+		}
+	    };
+
+	writeThread = new Thread(writeRun);
+	run.addShutdownHook(writeThread);
+    }
+
+    /** change the locale for the resource bundle*/
+    public void setLocale(Locale loc) {
+	messages = ResourceBundle.getBundle("feedback",loc);
+    }
+    /**
+     * This method will setup some of the GUI appearance that window should have when the user
+     * choose to start the Reporting Feedback sequence.
+     */
+    public void setup_UIManager ()
+    {
+	// why do you do all this???
+	//UIManager.put("TabbedPane.selected",(new Color(224,240,224)));
+	//UIManager.put("Button.select",(new Color(224,240,224)));
+	//UIManager.put("ScrollBar.background",(new Color(176,208,176)));
+	//UIManager.put("ScrollBar.highlight",(new Color(224,240,224)));
+	//UIManager.put("ScrollBar.thumb",(new Color(224,240,224)));
+	//UIManager.put("ScrollBar.track",(new Color(224,240,224)));
+	//UIManager.put("Tree.textBackground",new Color(224,240,224));
+	//UIManager.put("Label.font",new Font("Dialog",Font.PLAIN,12));
+	//UIManager.put("ComboBox.font",new Font("Dialog",Font.PLAIN,12));
+	//UIManager.put("TextArea.font",new Font("Dialog",Font.PLAIN,12));
+    }
+
+    /**
+     * This method will get the information about all the components inside the window that are
+     * open by the application when the action occured and save it to vector.
+     * @param comp its the component where the user did the action.
+     * @param command its the action's command the user did.
+     */
+    private void saving (final Component comp,final String command)
+    {
+	if ((command.equals(messages.getString("SendFeedback"))) ||
+	    ((command.equals("FastSend")) && (stop_rec == false)))
+	    {
+		start_fd = true;
+	    }
+
+	Runnable saveall;
+	saveall = new Runnable ()
+	    {
+		public void run ()
+		{
+		    try
+			{
+			    Runnable savework;
+			    savework = new Runnable ()
+				{
+				    public void run ()
+				    {
+					Date curr_date;
+					curr_date = new Date();
+					log = new History(curr_date,command);
+
+					if ((start_fd == true)&&(command.equals(messages.getString("WindowOpened"))))
+					    {
+						start_rec = true;
+						rec = true;
+						start_fd = false;
+					    }
+
+					getFrame(comp,command);
+					saveState(command);
+					saveLog();
+
+					if ((point != null) && (rec == true))
+					    {
+						if (command.equals(messages.getString("WindowClosed")))
+						    {
+							if (point instanceof JDialog)
+							    if (((JDialog)point).isModal() == true)
+								if (point == modalWindow)
+								    frame.setComment(false);
+						    }
+					    }
+				    }
+				};
+
+			    SwingUtilities.invokeAndWait(savework);
+			}
+		    catch (Exception ex) {ex.printStackTrace();}
+		}
+	    };
+
+	saveallThread = new Thread(saveall);
+	saveallThread.start();
+    }
+
+    /**
+     * This method will get the screen shot of the window where the actions occured and
+     * add the image also its dimension to the passed record.
+     * It will caused the saveThread to start so it can allows other stuff to be run concurrently
+     * and this thread will be stopped when the screen shot of the window alreadyfinished taken.
+     * @param hst is the record where the image and its dimension should be stored to.
+     */
+    public void saveImage (final History hst)
+    {
+	Runnable saveRun;
+	saveRun = new Runnable()
+	    {
+		History hist = hst;
+		public void run()
+		{
+		    String img,width,height;
+		    Rectangle rect;
+		    int w, h,dimwidth,dimheight;
+		 
+		    if (point != null)
+			{
+			    dimwidth = sh.getWidth();
+			    dimheight = sh.getHeight();
+
+			    w = point.getWidth();
+			    h = point.getHeight();
+
+			    try
+				{
+				    rect = new Rectangle(point.getLocationOnScreen(),
+							 new Dimension(w,h));
+			    
+				    if (w >= dimwidth)
+					w = dimwidth - 50;
+				    
+				    width = "" +  w;
+				    
+				    if (h >= dimheight)
+					h = dimheight - 50;
+				    
+				    height = "" + h;
+				}
+			    catch (IllegalComponentStateException exp)
+				{
+				    rect = null;
+				    width = null;
+				    height = null;
+				}
+			}
+		    else
+			{
+			    rect = null;
+			    width = null;
+			    height = null;
+			}
+		    
+		    if (rect != null)
+			{
+			    img = sh.getImage(rect);
+			    if (savefinish == true)
+				{
+				    hist.setImage(img,height,width);
+				}
+			}
+		}
+	    };
+	
+	Thread saveThread;
+	saveThread = new Thread(saveRun);
+	saveThread.start();
+    }
+
+    /**
+     * This method will setup the savefinish value.
+     * (Precondition: (save != null))
+     * @param save the value savefinish should have.
+     */
+    public static void setSavefinish (boolean save)
+    {
+	savefinish = save;
+    }
+
+
+    /**
+     * This method will make the JMenu that is should be added to all the
+     * window opened by the application.
+     * <br>If rec = false then the JMenu will give user option to start the Reporting Feedback sequence
+     * or Sending feedback or to view the history of actions they have done so far.
+     * <br>If rec = true then the JMenu will only give user option to view the history of actions
+     * they have done so far or sending feedback or taking screenshot.
+     * @return JMenu that should be added to all window open.
+     */
+    public JMenu makeMenu ()
+    {
+	JMenu menu;
+	JMenuItem item,item2,item3;
+	ImageIcon icon,icon2,icon3;
+
+	icon3 = JarTools.getImage("fastsend.gif");
+	item3 = new JMenuItem(messages.getString("SendFeedback"), 
+			      new ImageIcon(icon3.getImage().getScaledInstance
+					    (20,20,Image.SCALE_SMOOTH)));
+	item3.setActionCommand("FastSend");
+	item3.addActionListener(this);
+	item3.setMnemonic(KeyEvent.VK_N);
+	item3.setAccelerator(sndfdKey);
+	item3.setToolTipText("Send feedback now");
+
+	item = null;
+
+	if (rec == false)
+	    {
+		icon = JarTools.getImage("feedback.gif");
+		item = new JMenuItem("Report Feedback",
+				     new ImageIcon(icon.getImage().getScaledInstance
+						   (20,20,Image.SCALE_SMOOTH)));
+		item.setActionCommand(messages.getString("SendFeedback"));
+		item.addActionListener(this);
+		item.setMnemonic(KeyEvent.VK_F);
+		item.setAccelerator(fdKey);
+		item.setToolTipText("Report feedback");
+	    }
+	else
+	    {
+		icon = JarTools.getImage("camera.gif");
+		item = new JMenuItem(messages.getString("ScreenShot"),
+				     new ImageIcon(icon.getImage().getScaledInstance
+						   (20,20,Image.SCALE_SMOOTH)));
+		item.setActionCommand("ScreenShot");
+		item.setMnemonic(KeyEvent.VK_S);
+		item.addActionListener(this);
+		item.setAccelerator(shKey);
+		item.setToolTipText("Take screenshot of the whole screen.");
+	    }
+
+	icon2 = JarTools.getImage("history.gif");
+	item2 = new JMenuItem(messages.getString("LookHistory"),
+			      new ImageIcon(icon2.getImage().getScaledInstance
+					    (20,20,Image.SCALE_SMOOTH)));
+	item2.setActionCommand(messages.getString("LookHistory"));
+	item2.addActionListener(this);
+	item2.setMnemonic(KeyEvent.VK_H);
+	item2.setAccelerator(histKey);
+	item2.setToolTipText(messages.getString("LookHistoryButton"));
+
+	menu = new JMenu (messages.getString("Feedback"));
+	menu.setMnemonic(KeyEvent.VK_D);
+
+
+	if (item != null)
+	    menu.add(item);
+
+	menu.add(item3);
+
+	menu.add(item2);
+
+	if (rec == true)
+	    menu.setBackground(new Color(176,209,217));
+	else
+	    menu.setBackground(new Color(204,204,204));
+
+	return menu;
+    }
+
+    public void refreshLocationAndSize(Window window) {
+
+	window.setLocation(window.getX(),window.getY());
+	
+	Toolkit kit;
+	kit = Toolkit.getDefaultToolkit();
+	Dimension screenSize;
+	screenSize = kit.getScreenSize();
+	int screenHeight;
+	screenHeight = screenSize.height;
+	int screenWidth;
+	screenWidth = screenSize.width;
+	
+	if ((window.getSize().width > screenWidth) &&
+	    (window.getSize().height > screenHeight)) {
+	    window.setSize(window.getPreferredSize());
+	}
+    }
+
+
+    /**
+     * This method will setup the GUI that this window should have at the moment.
+     * If start_rec is true then JMenu don't have the option to start Reporting Feedback sequence in it.
+     * If stop_rec is true and rec is true then JMenu there should be an option to start Reporting Feedback sequence in it.
+     * The JMenu will only be affected window of type JDialog and JFrame only.
+     * (Precondition: (window != null))
+     * @param window its the window that we want to set the GUI
+     */
+    public void setUI (Window window)
+    {	
+	if (start_rec == true) {
+	    if (window instanceof JFrame) {
+		JFrame a;
+		a = (JFrame) window;
+		
+		JMenuBar menuBar;
+		menuBar = a.getJMenuBar();
+		
+		if (menuBar != null) {
+		    
+		    JMenu temp_menu;
+		    temp_menu = menuBar.getMenu(menuBar.getMenuCount() - 1);
+		    temp_menu.setBackground(new Color(176,209,217));
+		    temp_menu.remove(0);
+		    
+		    JMenuItem item;
+		    ImageIcon icon;
+		    icon = JarTools.getImage("camera.gif");
+		    item = new JMenuItem(messages.getString("ScreenShot"),
+					 new ImageIcon(icon.getImage().getScaledInstance
+						       (20,20,Image.SCALE_SMOOTH)));
+		    item.setActionCommand("ScreenShot");
+		    item.setMnemonic(KeyEvent.VK_S);
+		    item.addActionListener(this);
+		    item.setAccelerator(shKey);
+		    item.setToolTipText("Take screenshot of this window");
+		    
+		    temp_menu.insert(item,0);
+		}
+		
+		refreshLocationAndSize(a);
+		a.validate();
+		a.repaint();
+	    }
+	    else if (window instanceof JDialog) {
+		
+		JDialog a;
+		a = (JDialog) window;
+		JMenuBar menuBar;
+		menuBar = a.getJMenuBar();
+		
+		if (menuBar != null) {
+		    
+		    JMenu temp_menu;
+		    temp_menu = menuBar.getMenu(menuBar.getMenuCount() - 1);
+		    temp_menu.setBackground(new Color(176,209,217));
+		    temp_menu.remove(0);
+		    
+		    JMenuItem item;
+		    ImageIcon icon;
+		    icon = JarTools.getImage("camera.gif");
+		    item = new JMenuItem(messages.getString("ScreenShot"),
+					 new ImageIcon(icon.getImage().getScaledInstance
+						       (20,20,Image.SCALE_SMOOTH)));
+		    item.setActionCommand("ScreenShot");
+		    item.setMnemonic(KeyEvent.VK_S);
+		    item.addActionListener(this);
+		    item.setAccelerator(shKey);
+		    item.setToolTipText("Take screenshot of this window");
+		    
+		    temp_menu.insert(item,0);
+		}
+		refreshLocationAndSize(a);
+		a.validate();
+		a.repaint();	
+	    }
+	    else if (window instanceof JWindow) {
+		JWindow a;
+		a = (JWindow) window;
+		
+		((JComponent) a.getContentPane()).unregisterKeyboardAction(fdKey);
+		
+		((JComponent) a.getContentPane()).registerKeyboardAction(this, "ScreenShot", shKey,JComponent.WHEN_IN_FOCUSED_WINDOW);
+	    }
+	    else
+		return;
+	}
+	else {
+	    
+	    if ((stop_rec == true) && (rec == true)) {
+		
+		if (window instanceof JFrame) {
+		    JFrame a;
+		    a = (JFrame) window;
+		    
+		    JMenuBar menuBar;
+		    menuBar = a.getJMenuBar();
+		    
+		    if (menuBar != null) {
+			
+			JMenu temp_menu;
+			temp_menu = menuBar.getMenu(menuBar.getMenuCount() - 1);
+			temp_menu.setBackground(new Color(204,204,204));
+			temp_menu.remove(0);
+			
+			ImageIcon icon;
+			JMenuItem item;
+			
+			icon = JarTools.getImage("feedback.gif");
+			item = new JMenuItem("Report Feedback", new ImageIcon(icon.getImage().getScaledInstance(20,20,Image.SCALE_SMOOTH)));
+			item.setActionCommand(messages.getString("SendFeedback"));
+			item.addActionListener(this);
+			item.setMnemonic(KeyEvent.VK_F);
+			item.setAccelerator(fdKey);
+			item.setToolTipText(messages.getString("SendFeedbackButton"));
+			
+			temp_menu.insert(item,0);
+		    }
+		    a.setSize(a.getPreferredSize());
+		    a.validate();
+		    a.repaint();
+		}
+		else if (window instanceof JDialog) {
+		    JDialog a;
+		    a = (JDialog) window;
+		    
+		    JMenuBar menuBar;
+		    menuBar = a.getJMenuBar();
+		    
+		    if (menuBar != null) {
+			
+			JMenu temp_menu;
+			temp_menu = menuBar.getMenu(menuBar.getMenuCount() - 1);
+			temp_menu.setBackground(new Color(204,204,204));
+			temp_menu.remove(0);
+			
+			ImageIcon icon;
+			JMenuItem item;
+			
+			icon = JarTools.getImage("feedback.gif");
+			item = new JMenuItem("Report Feedback", new ImageIcon(icon.getImage().getScaledInstance(20,20,Image.SCALE_SMOOTH)));
+			item.setActionCommand(messages.getString("SendFeedback"));
+			item.addActionListener(this);
+			item.setMnemonic(KeyEvent.VK_F);
+			item.setAccelerator(fdKey);
+			item.setToolTipText(messages.getString("SendFeedbackButton"));
+			
+			temp_menu.insert(item,0);
+		    }
+		    a.setSize(a.getPreferredSize());
+		    a.validate();
+		    a.repaint();
+		}
+		else if (window instanceof JWindow) {
+		    JWindow a;
+		    a = (JWindow) window;
+		    
+		    ((JComponent) a.getContentPane()).registerKeyboardAction(this, messages.getString("SendFeedback"), fdKey, JComponent.WHEN_IN_FOCUSED_WINDOW);
+		    
+		    ((JComponent) a.getContentPane()).unregisterKeyboardAction(shKey);
+		    
+		}
+		else
+		    return;
+	    }
+	    else
+		return;
+	}
+    }
+    
+    /**
+     * This will make an Window[] convert to ArrayList that contain list of Window.
+     * (Precondition: (windows != null))
+     * @param windows the array that we want to convert to ArrayList
+     * @return        the array list that contains all object in array windows.
+     */
+    public ArrayList addToArrayList (Window[] windows,ArrayList arr)
+    {
+	int i;
+
+	for (i = 0 ; i < windows.length ; i++)
+	    {
+		arr.add(windows[i]);
+	    }
+	
+	return arr;
+    }
+
+    /**
+     * This method will get all the information of the window that are currently open when the action
+     * occured and these windows are not the window where the action occured.
+     * It will add these windows information to the current record and it will also setting up the 
+     * GUI for each of these windows.
+     * @param command the action's command user did.
+     */
+    public void saveState(String command)
+    {
+	Window[] frame_windows;
+	ArrayList windows;
+	Frame[] frames;
+	int i,j;
+
+	if (log == null)
+	    return;
+	
+	windows = new ArrayList();
+
+	frames = Frame.getFrames();
+	for (i = 0 ; i<frames.length;i++) 
+	    {
+		frame_windows = frames[i].getOwnedWindows();
+		windows = addToArrayList(frame_windows,windows);
+		
+		for (j = 0;j < frame_windows.length ; j++)
+		    {
+			Window window;
+			window = frame_windows[j];
+
+			if (window == point)
+			    {
+				Container cont;
+				cont = window.getParent();
+
+				if ((cont != null) && (cont instanceof Window) &&
+				    (windows.contains(cont) == false)&&(cont != point))
+				    {
+					if (cont instanceof ReportDetails.View) {}
+					else if (cont instanceof Movie) {}
+					else
+					    {
+						setUI((Window) cont);
+						
+						/*if (rec == true)
+						    {
+							log.addComponent(compInfo.getInside((Window) cont),
+									 messages.getString("CurrentlyOpenWindow"));
+						    }
+						    else*/
+						    compList.getInside((Window) cont);
+						
+						windows.add(cont);
+					    }
+				    }	
+				else
+				    {}
+			    }
+			else
+			    {
+				if (window instanceof ReportDetails.View) {}
+				else if (window instanceof Movie) {}
+				else
+				    {
+					if ((window != null) && (window != point))
+					    {
+						setUI(window);
+						
+						if (rec == true)
+						    {
+							log.addComponent(compInfo.getInside((Window) window),
+									 messages.getString("CurrentlyOpenWindow"));
+						    }
+						else
+						    compList.getInside((Window) window);
+						
+						Container cont;
+						cont = window.getParent();
+						
+						if ((cont != null) && (cont instanceof Window) &&
+						    (cont != point) && (windows.contains(cont) == false))
+						    {
+							if (cont instanceof ReportDetails.View) {}
+							else if (cont instanceof Movie) {}
+							else
+							    {
+								setUI((Window)cont);
+								
+								/*if (rec == true)
+								    {
+									log.addComponent(compInfo.getInside((Window) cont),
+											 messages.getString("CurrentlyOpenWindow"));
+								    }
+								    else*/
+								    compList.getInside((Window) cont);
+
+								windows.add(cont);
+							    }
+						    }
+						else {}
+					    }
+				    }
+			    }
+		    }
+	    }
+    }
+
+    /**
+     * This method will get all the information about the window where the actions occured and it also will setting up
+     * how hthe GUI should look like in this window and it will also take the image of this window.
+     * @param comp is the component where the actions happened.
+     * @param command is the command of user's action
+     */
+    public void getFrame (Component comp,String command)
+    {
+	boolean cls;
+	
+	cls = false;
+	point = null;
+	
+	Container frame; 
+	myframe = null;
+	
+	if (comp == null)
+	    return;
+	else if (comp instanceof Window)
+	    frame = (Container) comp;
+	else
+	    frame = SwingUtilities.getWindowAncestor(comp);
+	
+	if (frame instanceof Window)
+	    {
+		JMenu menu;
+		point = (Window) frame;
+
+		point.addMouseListener(mouse_blocker_listener);
+		point.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+
+		
+		if ((frame instanceof ReportDetails.View)||
+		    (frame instanceof Movie))
+		    {
+			log = null;
+			return;
+		    }
+		
+		if (command.equals(messages.getString("WindowClosed")))
+		    {
+			cls = true;
+			if (frame instanceof FeedbackInterface)
+			    {
+				stop_rec = true;
+			    }
+			else
+			    {}
+		    }
+		
+		if  (command.equals(messages.getString("WindowOpened")))
+		    {
+			menu = makeMenu();
+		    }
+		else
+		    {
+			menu = null;
+		    }
+		
+		if (frame instanceof JFrame)
+		    {
+			JFrame a;
+			a = (JFrame) frame;
+			
+			myframe = a.getTitle();
+			
+			if (menu != null)
+			    {
+				JMenuBar menuBar;
+				menuBar = a.getJMenuBar();
+				
+				if (menuBar == null)
+				    {
+					menuBar = new JMenuBar();
+					menuBar.add(menu);
+					a.setJMenuBar(menuBar);
+				    }
+				else
+				    menuBar.add(menu);
+			    }
+			
+			if ((menu != null))
+			    {
+				refreshLocationAndSize(a);
+				a.validate();
+				a.repaint();
+			    }
+		    }
+		else if (frame instanceof JDialog)
+		    {   
+			JDialog a;
+			a = (JDialog) frame;
+			
+			myframe = a.getTitle();
+			
+			if (menu != null)
+			    {
+				JMenuBar menuBar;
+				menuBar = a.getJMenuBar();
+				
+				if (menuBar == null)
+				    {
+					menuBar = new JMenuBar();
+					menuBar.add(menu);
+					a.setJMenuBar(menuBar);
+				    }
+				else
+				    menuBar.add(menu);
+			    }
+		    
+			if ((menu != null))
+			    {
+				refreshLocationAndSize(a);
+				a.validate();
+				a.repaint();
+			    }
+		    }
+		else if (frame instanceof JWindow)
+		    {
+			JWindow a;
+			a = (JWindow) frame;
+			
+			if (menu != null)
+			    {
+				((JComponent) a.getContentPane()).registerKeyboardAction(this,
+											 messages.getString("LookHistory"),
+											 histKey,
+											 JComponent.WHEN_IN_FOCUSED_WINDOW);
+				
+				if (rec == false)
+				    {
+					((JComponent) a.getContentPane()).registerKeyboardAction(this,
+												 messages.getString("SendFeedback"),
+												 fdKey,
+												 JComponent.WHEN_IN_FOCUSED_WINDOW);
+					
+					((JComponent) a.getContentPane()).unregisterKeyboardAction(shKey);
+				    }
+				else
+				    {
+					((JComponent) a.getContentPane()).unregisterKeyboardAction(fdKey);
+					
+					((JComponent) a.getContentPane()).registerKeyboardAction(this,
+												 "ScreenShot",
+												shKey,
+												 JComponent.WHEN_IN_FOCUSED_WINDOW);
+				    }
+			    }
+
+			if (cls == true)
+			    {
+				((JComponent) a.getContentPane()).unregisterKeyboardAction(fdKey);
+				
+				((JComponent) a.getContentPane()).unregisterKeyboardAction(shKey);
+			    }
+		    }
+		
+		/*if ((command.equals(messages.getString("WindowClosed")))||
+		  (log == null))
+		  {}
+		  else
+		  saveImage(log);*/
+
+		/*if (rec == true)
+		    log.addComponent(compInfo.getInside(point),command);
+		    else*/
+		    compList.getInside(point);
+
+		if (myframe == null)
+		    myframe = " ";
+		
+		log.setTitle(myframe);
+		
+		return;
+	    }
+	else
+	    return;
+    }
+
+    /**
+     * This method will add the current record to the vector.
+     */
+    public void saveLog()
+    {
+	if (start_rec == true)
+	    {
+		save_HistLog("history.log");
+		vector.removeAllElements();
+		System.gc();
+		start_rec = false;
+		stop_rec = false;
+		rec = true;
+	    }
+	
+	if (log != null)
+	    vector.add(0,log);
+
+	if (vector.size() > 30)
+	    {
+		if (rec == false)
+		    {
+			save_tempHistLog("history.log");
+		    }
+		else
+		    save_tempHistLog("feedbackhist.log");
+		vector.removeAllElements();
+		//vector.removeElementAt(vector.size() - 1);
+		System.gc();
+	    }
+
+	if ((rec == true) && (stop_rec == true))
+	    {
+		save_HistLog("feedbackhist.log");
+		vector.removeAllElements();
+		System.gc();
+		vector = getPrev_log("history.log");
+		File f2;
+		f2 = new File("feedbackhist.log");
+		f2.delete();
+		f2 = new File("temp_feedbackhist.log");
+		f2.delete();
+		start_rec = false;
+		stop_rec = true;
+		rec = false;
+	    }
+
+	if (point != null)
+	    {
+		point.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+		point.removeMouseListener(mouse_blocker_listener);
+	    }
+    }
+
+    /**
+     * This method will save whatever in the vector at the moment to a file
+     * with the specified filename.
+     * Then it will also save the vector inside the file with the name "temp_"
+     * concatenated with the specified filename into it.
+     * (Precondition : (filename != null))
+     * @param filename its the name of the file where the vector should be saved.
+     */
+    public void save_HistLog(String filename)
+    {
+	 try 
+	     {
+		 File f;
+		 f = new File(filename);
+		 FileOutputStream fos;
+		 fos = new FileOutputStream(f);
+		 ObjectOutputStream oos;
+		 oos = new ObjectOutputStream(fos);
+		 oos.writeObject(vector);
+		 vector.removeAllElements();
+		 System.gc();
+		 vector = getPrev_log("temp_"+ filename);
+		 oos.writeObject(vector);
+		 oos.close();
+		 f = new File("temp_" + filename);
+		 f.delete();
+		 writeThread = null;
+	     }
+	 catch (IOException exp) {}
+    }
+   
+    /**
+     * This method will save whatever in the vector at the moment to a file with a 
+     * name "temp_" concatenated with the specified filename into it.
+     * (Precondition : (filename != null))
+     * @param filename its part of the name of the file where the vector should be saved.
+     */
+    public void save_tempHistLog(String filename)
+    {
+	try
+	    {
+		File f;
+		f = new File("temp_"+ filename);
+		FileOutputStream fos;
+		fos = new FileOutputStream(f);
+		ObjectOutputStream oos;
+		oos = new ObjectOutputStream(fos);
+		oos.writeObject(vector);
+		oos.close();
+	    }
+	catch (IOException exp) {}
+    }
+
+    /**
+     * This method will get vector that is stored in a file with the specified filename and
+     * it will also save the second vector in that file to a file with a name
+     * temp_history.log.
+     * If file with the specified filename does not exist then it will return a new vector.
+     * (Precondition : (filename != null))
+     * @param filename is the name of the file where we want to get the vector.
+     * @return         the first vector saved in the file with the specified filename.
+     */
+    public Vector getPrev_log(String filename)
+    {
+	Vector stack;
+	ObjectInputStream ois;
+	stack = null;
+	try
+	    {
+		File f;
+		f = new File(filename);
+		if (f.exists() == true)
+		    {
+			FileInputStream fis;
+			fis = new FileInputStream(f);
+			ois = new ObjectInputStream(fis);
+			stack = (Vector) ois.readObject();
+
+			Vector stack2;
+			stack2 = null;
+
+			try
+			    {
+				stack2 = (Vector) ois.readObject();
+			    }
+			catch (EOFException e) {stack2 = null;}
+			
+			if (stack2 != null)
+			    {
+				File f2;
+				f2 = new File("temp_history.log");
+				FileOutputStream fos;
+				fos = new FileOutputStream(f2);
+				ObjectOutputStream oos;
+				oos = new ObjectOutputStream(fos);
+				oos.writeObject(stack2);
+				oos.close();
+				stack2.removeAllElements();
+				stack2 = null;
+				System.gc();
+			    }
+
+			ois.close();
+			return stack;
+		    }
+		else
+		    stack = new Vector(); 
+	    }
+	catch (IOException exp) {exp.printStackTrace();}
+	catch (ClassNotFoundException exp2) {System.out.println("class exp");}
+	
+	return stack;
+    }
+ 
+    /**
+     * This method will make sure that there is something running at the moment.
+     */
+    private static void ensureEventThread()
+    {
+	if (SwingUtilities.isEventDispatchThread())
+	    {
+		return;
+	    }
+	throw new RuntimeException("something running");
+    }
+    
+    /**
+     * This method will called the Conformation window to show up.
+     * @param err_array this is the array containing all the information taken when user doing the Feedback form.
+     * @param imgFile this is the array conataining the String representation of the images taken when user doing the 
+     *                Feedback form.
+     */
+    /*public void getVector(String[] err_array,String[] imgFile)
+    {
+	Conformation rd;
+	rd = new Conformation(vector,err_array,imgFile,messages);
+	return;
+	}*/
+
+    /**
+     * This method will get the vector that hold the history.
+     * @return the vector that hold the history stored so far.
+     */
+    public Vector getVector()
+    {
+	return vector;
+    }
+
+    /**
+     * This class will catch the window opened and window closed event.
+     * If an application open or close a window it will be listened by this class so the information
+     * about that window and all the other open window can be took.
+     */
+    class MyEventQueue extends EventQueue
+    { 
+	/**
+	 * This method  will catch the window opened and window closed event.
+	 * If an application open or close a window it will be listened by this class so the information
+	 * about that window and all the other open window can be took.
+	 * @param evt its the event.
+	 */
+	protected void dispatchEvent(AWTEvent evt) 
+	{
+	    super.dispatchEvent(evt);
+	    int id;
+	    id = evt.getID();
+	
+	    if ((id == WindowEvent.WINDOW_OPENED)||(id == WindowEvent.WINDOW_CLOSED))
+		{   
+		    String command;
+		    command = " ";
+		    
+		    if (id == WindowEvent.WINDOW_OPENED) command = messages.getString("WindowOpened");
+		    else if (id == WindowEvent.WINDOW_CLOSED) command = messages.getString("WindowClosed");
+		   
+		    WindowEvent e;
+		    e = (WindowEvent) evt;
+		    Window comp;
+		    comp =  e.getWindow();
+		 
+		    if ((comp instanceof ReportDetails.View)||
+			(comp instanceof Movie))
+			return;
+
+		    saving(comp,command);
+		}
+	}
+    }
+
+    /**
+     * This method will take the screenshot of the whole screen.
+     * @param p the window where user select to take screenshot.
+     * @return  the screenshot image.
+     */
+    //public byte[] recreateEvent (final Window p)
+    public BufferedImage recreateEvent (final Window p)
+    {
+	setup_UIManager();
+	Rectangle rect;
+	int w, h;
+	
+	if (p != null)
+	    {
+		w = p.getWidth();
+		h = p.getHeight();
+		
+		try
+		    {
+			rect = new Rectangle(p.getLocationOnScreen(),
+				 	     new Dimension(w,h));
+		    }
+		catch (IllegalComponentStateException exp)
+		    {
+			rect = null;
+		    }
+	    }
+	else
+	    {
+		rect = null;
+	    }
+	
+	if (rect != null)
+	    {
+		//byte[] img;
+		BufferedImage img;
+		img = sh.captureScreen();
+		
+		return img;
+	    }
+	else
+	    return null;
+    }
+
+    /**
+     * This method will listen to any action event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the ActionEvent.
+     */
+    public void actionPerformed(ActionEvent e)
+    {
+	String command;
+	command = e.getActionCommand();
+	Component comp;
+	comp = (Component)e.getSource();
+	
+	saving(comp,command);
+
+	if (myframe == null)
+	    myframe = " ";
+	
+	if("FastSend".equals(e.getActionCommand()))
+	    {
+		point.addMouseListener(mouse_blocker_listener);
+		point.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+
+		BufferedImage img;
+		boolean modal;
+			
+		modal = false;
+
+		img = recreateEvent(point);
+		
+		if (stop_rec == true)
+		    {
+			feedbackframe = myframe;
+			
+			JDialog.setDefaultLookAndFeelDecorated(true);
+			stop_rec = false;
+			frame = null;
+			frame = new FeedbackInterface(sh,feedbackframe,messages,this);
+			if ((img != null) && (savefinish == true))
+			    {
+				int x,y,w,h,iw,ih;
+				double wrat,hrat;
+				Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+				
+				iw = (int) (dim.getWidth() - 60);
+				ih = (int) (dim.getHeight() * 0.75) - 50;
+				
+				wrat = iw/((double)dim.getWidth());
+				hrat = ih/((double)dim.getHeight());
+				
+				w = (int) (point.getWidth() * wrat);
+				h = (int) (point.getHeight() * hrat);
+				x = (int) (point.getLocationOnScreen().x * wrat);
+				y = (int) (point.getLocationOnScreen().y * hrat);
+
+				//frame.addScreenPanel(feedbackframe,sh.getBuffImage(),img);
+				frame.addScreenPanel(feedbackframe,img,x,y,w,h);
+			    }
+			frame.setVisible(false);
+		    }
+		else
+		    {
+			if ((img != null) &&(savefinish == true))
+			    {
+				int x,y,w,h,iw,ih;
+				double wrat,hrat;
+				Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+				
+				iw = (int) (dim.getWidth() - 60);
+				ih = (int) (dim.getHeight() * 0.75) - 50;
+				
+				wrat = iw/((double)dim.getWidth());
+				hrat = ih/((double)dim.getHeight());
+				
+				w = (int) (point.getWidth() * wrat);
+				h = (int) (point.getHeight() * hrat);
+				x = (int) (point.getLocationOnScreen().x * wrat);
+				y = (int) (point.getLocationOnScreen().y * hrat);
+				
+				//frame.addScreenPanel(feedbackframe,sh.getBuffImage(),img);
+				if (point instanceof JDialog)
+				    modal = ((JDialog) point).isModal();
+				
+				feedbackframe = myframe;
+				frame.addScreenPanel(feedbackframe,img,x,y,w,h);
+			    }
+		    }
+		
+		ActionEvent evt;
+		evt = new ActionEvent(frame.send_button,ActionEvent.ACTION_PERFORMED,messages.getString("Send"));
+		
+		frame.actionPerformed(evt);
+
+		point.removeMouseListener(mouse_blocker_listener);
+		point.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));		 
+	    }
+
+	if(messages.getString("SendFeedback").equals(e.getActionCommand()))
+	    {
+		setup_UIManager();
+		//byte[] img;
+		BufferedImage img;
+		boolean modal;
+
+		point.addMouseListener(mouse_blocker_listener);
+		point.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+
+		modal = false;
+		
+		img = recreateEvent(point);
+		feedbackframe = myframe;
+		if (point instanceof JDialog)
+		    modal = ((JDialog)point).isModal();
+			
+		JDialog.setDefaultLookAndFeelDecorated(true);
+		stop_rec = false;
+		frame = null;
+		frame = new FeedbackInterface(sh,feedbackframe,messages,this);
+		if ((img != null) && (savefinish == true))
+		    {
+			int x,y,w,h,iw,ih;
+			double wrat,hrat;
+			Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+
+			iw = (int) (dim.getWidth() - 60);
+			ih = (int) (dim.getHeight() * 0.75) - 50;
+			
+			wrat = iw/((double)dim.getWidth());
+			hrat = ih/((double)dim.getHeight());
+			
+			w = (int) (point.getWidth() * wrat);
+			h = (int) (point.getHeight() * hrat);
+			x = (int) (point.getLocationOnScreen().x * wrat);
+			y = (int) (point.getLocationOnScreen().y * hrat);
+		
+			//frame.addScreenPanel(feedbackframe,sh.getBuffImage(),img);
+			frame.addScreenPanel(feedbackframe,img,x,y,w,h);
+			frame.setComment(modal);
+			
+			if (modal == true)
+			    {
+				modalWindow = point;
+			    }
+		    }
+
+		point.removeMouseListener(mouse_blocker_listener);
+		point.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));	
+	    }
+	
+	if ("ScreenShot".equals(e.getActionCommand()))
+	    {
+		point.addMouseListener(mouse_blocker_listener);
+		point.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+
+		//byte[] img;
+		BufferedImage img;
+		boolean modal;
+		modal = false;
+		img = recreateEvent(point);
+		
+		if ((img != null) &&(savefinish == true))
+		    {
+			int x,y,w,h,iw,ih;
+			double wrat,hrat;
+			Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+
+			iw = (int) (dim.getWidth() - 60);
+			ih = (int) (dim.getHeight() * 0.75) - 50;
+			
+			wrat = iw/((double)dim.getWidth());
+			hrat = ih/((double)dim.getHeight());
+			
+			w = (int) (point.getWidth() * wrat);
+			h = (int) (point.getHeight() * hrat);
+			x = (int) (point.getLocationOnScreen().x * wrat);
+			y = (int) (point.getLocationOnScreen().y * hrat);
+
+			//frame.addScreenPanel(feedbackframe,sh.getBuffImage(),img);
+			if (point instanceof JDialog)
+			    modal = ((JDialog) point).isModal();
+
+			feedbackframe = myframe;
+			frame.addScreenPanel(feedbackframe,img,x,y,w,h);
+			frame.setComment(modal);
+
+			if (modal == true)
+			    modalWindow = point;
+		    }
+
+		point.removeMouseListener(mouse_blocker_listener);
+		point.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));	
+	    }
+
+	if (messages.getString("LookHistory").equals(e.getActionCommand()))
+	    {
+		point.addMouseListener(mouse_blocker_listener);
+		point.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+
+		setup_UIManager();
+		ReportDetails rd;
+		rd = new ReportDetails(vector,null,null,messages,rec);
+
+		point.removeMouseListener(mouse_blocker_listener);
+		point.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));	
+	    }
+    }
+   
+
+    /**
+     * This method will listen to any Change event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the ChangeEvent.
+     */
+    public void stateChanged(ChangeEvent e)
+    {
+	String command;
+	command = messages.getString("ChangeState");
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+	
+	saving(comp,command);
+    }
+
+    /**
+     * This method will listen to any tree selection event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the TreeSelectionEvent.
+     */
+    public void valueChanged(TreeSelectionEvent e)
+    {
+	MutableTreeNode node;
+        node = (MutableTreeNode)
+	    (e.getPath().getLastPathComponent());
+	
+	if (node == null) return;
+
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+	
+	String command;
+	command = messages.getString("Node") + " ";
+	if (node instanceof DefaultMutableTreeNode)
+	    command = command + ((DefaultMutableTreeNode)node).getUserObject() + " ";
+	command = command + messages.getString("ValueChanged");
+	
+	saving(comp,command);
+    }
+
+    /**
+     * This method will listen to any tree nodes change in the tree model event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the TreeModelEvent.
+     */
+    public void treeNodesChanged(TreeModelEvent e)
+    {
+	MutableTreeNode node;
+        node = (MutableTreeNode)
+                 (e.getTreePath().getLastPathComponent());
+
+	try {
+            int index;
+	    index = e.getChildIndices()[0];
+            node = (MutableTreeNode)(node.getChildAt(index));
+        } catch (NullPointerException exc) {}
+
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+
+	String command;
+	command = messages.getString("Node");
+
+	if (node instanceof DefaultMutableTreeNode)
+	    command = command + " "  + ((DefaultMutableTreeNode)node).getUserObject();
+
+	command = command + " " + messages.getString("Change");
+	
+	saving(comp,command);
+    }
+
+    /**
+     * This method will listen to any tree nodes inserted in the tree model event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the TreeModelEvent.
+     */
+    public void treeNodesInserted(TreeModelEvent e)
+    {
+	MutableTreeNode node;
+        node = (MutableTreeNode)
+                 (e.getTreePath().getLastPathComponent());
+
+	try {
+            int index;
+	    index = e.getChildIndices()[0];
+            node = (MutableTreeNode)(node.getChildAt(index));
+        } catch (NullPointerException exc) {}
+	
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+	
+	String command;
+	command = messages.getString("Node");
+	if (node instanceof DefaultMutableTreeNode)
+	    command = command + " "  + ((DefaultMutableTreeNode)node).getUserObject();
+
+	command = command + " " + messages.getString("Inserted");
+	
+	saving(comp,command);
+    }
+    
+    /**
+     * This method will listen to any tree nodes removed in the tree model event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the TreeModelEvent.
+     */
+    public void treeNodesRemoved(TreeModelEvent e)
+    {
+	MutableTreeNode node;
+        node = (MutableTreeNode)
+                 (e.getTreePath().getLastPathComponent());
+
+	try {
+            int index;
+	    index = e.getChildIndices()[0];
+            node = (MutableTreeNode)(node.getChildAt(index));
+        } catch (NullPointerException exc) {}
+
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+
+	String command;
+	command = messages.getString("Node");
+	if (node instanceof DefaultMutableTreeNode)
+	    command = command + " "  + ((DefaultMutableTreeNode)node).getUserObject();
+	command = command + " " + messages.getString("Removed");
+	
+	saving(comp,command);
+    }
+ 
+    /**
+     * This method will listen to any tree structure change in the tree model event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the TreeModelEvent.
+     */
+    public void treeStructureChanged(TreeModelEvent e)
+    {
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+	
+	String command;
+	command = messages.getString("TreeStructuredChanged");
+	
+	saving(comp,command);
+    }
+
+    /**
+     * This method will listen to any item state change in the item  event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the ItemEvent.
+     */
+    public void itemStateChanged(ItemEvent e)
+    {
+	Component comp;
+	comp = (Component) e.getSource();
+	String command;
+	command = ((String) e.getItem()) + " " + 
+	    messages.getString("ItemStateChanged");
+
+	saving(comp,command);
+    } 
+
+    /**
+     * This method will listen to any contents change in the list data event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the ListDataEvent.
+     */
+    public void contentsChanged (ListDataEvent e)
+    {
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+	
+	String command;
+	command = messages.getString("ListChanged");
+
+	saving(comp,command);
+    } 
+
+    /**
+     * This method will listen to any interval added in the list data event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the ListDataEvent.
+     */
+    public void intervalAdded (ListDataEvent e)
+    {
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+	
+	String command;
+	command = messages.getString("ListContentsAdded");
+
+	saving(comp,command);
+    } 
+
+    /**
+     * This method will listen to any interval removed in the list data event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the ListDataEvent.
+     */
+    public void intervalRemoved (ListDataEvent e)
+    {
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+
+	String command;
+	command = messages.getString("ListContentRemoved");
+
+	saving(comp,command);
+    }
+
+    /**
+     * This method will listen to any value changed in the list data event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the ListDataEvent.
+     */
+    public void valueChanged (ListSelectionEvent e)
+    {
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+
+	String command;
+	command = messages.getString("ListValueChanged");
+
+	saving(comp,command);
+    }
+
+    /**
+     * This method will listen to any menu canceled in the menu event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the MenuEvent.
+     */
+    public void menuCanceled (MenuEvent e)
+    {
+	Component comp;
+	comp = (Component) e.getSource();
+	String command;
+	command = messages.getString("MenuCanceled");
+
+	saving(comp,command);
+    }
+
+    /**
+     * This method will listen to any menu deselected in the menu event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the MenuEvent.
+     */
+    public void menuDeselected (MenuEvent e)
+     {
+	Component comp;
+	comp = (Component) e.getSource();
+	String command;
+	command = messages.getString("MenuDeselected");
+
+	saving(comp,command);
+    }
+
+    /**
+     * This method will listen to any menu selected in the menu event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the MenuEvent.
+     */
+    public void menuSelected(MenuEvent e)
+    {
+	Component comp;
+	comp = (Component) e.getSource();
+	String command;
+	command = messages.getString("MenuSelected");
+
+	saving(comp,command);
+    }
+
+    /**
+     * This method will listen to any column added in the table column model event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the TableColumnModelEvent.
+     */
+    public void columnAdded (TableColumnModelEvent e)
+    {
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+
+	String command;
+	command = messages.getString("ColumnIndex") + " "  + e.getToIndex() 
+	    + messages.getString("AddedToTable");
+
+	saving(comp,command);
+    }
+
+    public void columnMarginChanged(ChangeEvent e) {}
+
+    /**
+     * This method will listen to any column moved in the table column model event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the TableColumnModelEvent.
+     */
+    public void columnMoved(TableColumnModelEvent e)
+    {
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+
+	String command;
+	command = messages.getString("ColumnIndexFrom") + " " + 
+	    e.getFromIndex() + " " + 
+	    messages.getString("MovedToColumnIndex") + " "  + e.getToIndex() +
+	    " " + messages.getString("InTheTable");
+
+	saving(comp,command);
+    }
+
+    /**
+     * This method will listen to any column removed in the table column model event that occured.
+     * It will allow this special listener class to record all the information inside all the
+     * window open when the action occured.
+     * @param e the TableColumnModelEvent.
+     */
+    public void columnRemoved(TableColumnModelEvent e)
+    {
+	Object obj;
+	obj = e.getSource();
+	
+	Component comp;
+
+	if (obj instanceof Component)
+	    {
+		comp = (Component)e.getSource();
+	    }
+	else
+	    {
+		if (modelHash.containsKey(obj) == true)
+		    {
+			comp = (Component) modelHash.get(obj);
+		    }
+		else
+		    {
+			comp = null;
+			System.out.println("weird" + obj.toString());
+		    }
+	    }
+
+	String command;
+	command = messages.getString("ColumnIndex") + " "  + e.getFromIndex() +
+	    " " + messages.getString("RemovedFromTheTable");
+
+	saving(comp,command);
+    }
+
+    public void columnSelectionChanged(ListSelectionEvent e) {}
+}
+
+
+
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Base64.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Base64.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Base64.java	(revision 31635)
@@ -0,0 +1,1289 @@
+package org.greenstone.gatherer.feedback;
+
+import java.io.*;
+/**
+ * Encodes and decodes to and from Base64 notation.
+ *
+ * <p>
+ * Change Log:
+ * </p>
+ * <ul>
+ *  <li>v2.0.1 - Fixed an error when decoding a single byte, that is, when the
+ *   encoded data was a single byte.</li>
+ *  <li>v2.0 - I got rid of methods that used booleans to set options. 
+ *   Now everything is more consolidated and cleaner. The code now detects
+ *   when data that's being decoded is gzip-compressed and will decompress it
+ *   automatically. Generally things are cleaner. You'll probably have to
+ *   change some method calls that you were making to support the new
+ *   options format (<tt>int</tt>s that you "OR" together).</li>
+ *  <li>v1.5.1 - Fixed bug when decompressing and decoding to a             
+ *   byte[] using <tt>decode( String s, boolean gzipCompressed )</tt>.      
+ *   Added the ability to "suspend" encoding in the Output Stream so        
+ *   you can turn on and off the encoding if you need to embed base64       
+ *   data in an otherwise "normal" stream (like an XML file).</li>  
+ *  <li>v1.5 - Output stream pases on flush() command but doesn't do anything itself.
+ *      This helps when using GZIP streams.
+ *      Added the ability to GZip-compress objects before encoding them.</li>
+ *  <li>v1.4 - Added helper methods to read/write files.</li>
+ *  <li>v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.</li>
+ *  <li>v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream
+ *      where last buffer being read, if not completely full, was not returned.</li>
+ *  <li>v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.</li>
+ *  <li>v1.3.3 - Fixed I/O streams which were totally messed up.</li>
+ * </ul>
+ *
+ * <p>
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit <a href="http://iharder.net/xmlizable">http://iharder.net/base64</a>
+ * periodically to check for updates or to contribute improvements.
+ * </p>
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 2.0
+ */
+public class Base64
+{
+    
+/* ********  P U B L I C   F I E L D S  ******** */   
+    
+    
+    /** No options specified. Value is zero. */
+    public final static int NO_OPTIONS = 0;
+    
+    /** Specify encoding. */
+    public final static int ENCODE = 1;
+    
+    
+    /** Specify decoding. */
+    public final static int DECODE = 0;
+    
+    
+    /** Specify that data should be gzip-compressed. */
+    public final static int GZIP = 2;
+    
+    
+    /** Don't break lines when encoding (violates strict Base64 specification) */
+    public final static int DONT_BREAK_LINES = 8;
+    
+    
+/* ********  P R I V A T E   F I E L D S  ******** */  
+    
+    
+    /** Maximum line length (76) of Base64 output. */
+    private final static int MAX_LINE_LENGTH = 76;
+    
+    
+    /** The equals sign (=) as a byte. */
+    private final static byte EQUALS_SIGN = (byte)'=';
+    
+    
+    /** The new line character (\n) as a byte. */
+    private final static byte NEW_LINE = (byte)'\n';
+    
+    
+    /** The 64 valid Base64 values. */
+    private final static byte[] ALPHABET =
+    {
+        (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+        (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+        (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', 
+        (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+        (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+        (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+        (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', 
+        (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
+        (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', 
+        (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/'
+    };
+    
+    /** 
+     * Translates a Base64 value to either its 6-bit reconstruction value
+     * or a negative number indicating some other meaning.
+     **/
+    private final static byte[] DECODABET =
+    {   
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,                 // Decimal  0 -  8
+        -5,-5,                                      // Whitespace: Tab and Linefeed
+        -9,-9,                                      // Decimal 11 - 12
+        -5,                                         // Whitespace: Carriage Return
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 14 - 26
+        -9,-9,-9,-9,-9,                             // Decimal 27 - 31
+        -5,                                         // Whitespace: Space
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,              // Decimal 33 - 42
+        62,                                         // Plus sign at decimal 43
+        -9,-9,-9,                                   // Decimal 44 - 46
+        63,                                         // Slash at decimal 47
+        52,53,54,55,56,57,58,59,60,61,              // Numbers zero through nine
+        -9,-9,-9,                                   // Decimal 58 - 60
+        -1,                                         // Equals sign at decimal 61
+        -9,-9,-9,                                      // Decimal 62 - 64
+        0,1,2,3,4,5,6,7,8,9,10,11,12,13,            // Letters 'A' through 'N'
+        14,15,16,17,18,19,20,21,22,23,24,25,        // Letters 'O' through 'Z'
+        -9,-9,-9,-9,-9,-9,                          // Decimal 91 - 96
+        26,27,28,29,30,31,32,33,34,35,36,37,38,     // Letters 'a' through 'm'
+        39,40,41,42,43,44,45,46,47,48,49,50,51,     // Letters 'n' through 'z'
+        -9,-9,-9,-9                                 // Decimal 123 - 126
+        /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 127 - 139
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */
+    };
+    
+    private final static byte BAD_ENCODING    = -9; // Indicates error in encoding
+    private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
+    private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
+
+    
+    /** Defeats instantiation. */
+    private Base64(){}
+    
+    
+    
+/* ********  E N C O D I N G   M E T H O D S  ******** */    
+    
+    
+    /**
+     * Encodes the first three bytes of array <var>threeBytes</var>
+     * and returns a four-byte array in Base64 notation.
+     *
+     * @param threeBytes the array to convert
+     * @return four byte array in Base64 notation.
+     * @since 1.3
+     */
+    private static byte[] encode3to4( byte[] threeBytes )
+    {   
+        return encode3to4( threeBytes, 3 );
+    }   // end encodeToBytes
+    
+    
+    
+    /**
+     * Encodes up to the first three bytes of array <var>threeBytes</var>
+     * and returns a four-byte array in Base64 notation.
+     * The actual number of significant bytes in your array is
+     * given by <var>numSigBytes</var>.
+     * The array <var>threeBytes</var> needs only be as big as
+     * <var>numSigBytes</var>.
+     *
+     * @param threeBytes the array to convert
+     * @param numSigBytes the number of significant bytes in your array
+     * @return four byte array in Base64 notation.
+     * @since 1.3
+     */
+    private static byte[] encode3to4( byte[] threeBytes, int numSigBytes )
+    {  
+        byte[] dest = new byte[4];
+        encode3to4( threeBytes, 0, numSigBytes, dest, 0 );
+        return dest;
+    }
+    
+     /**
+     * Encodes up to the first three bytes of array <var>threeBytes</var>
+     * and returns a four-byte array in Base64 notation.
+     * The actual number of significant bytes in your array is
+     * given by <var>numSigBytes</var>.
+     * The array <var>threeBytes</var> needs only be as big as
+     * <var>numSigBytes</var>.
+     * Code can reuse a byte array by passing a four-byte array as <var>b4</var>.
+     *
+     * @param b4 A reusable byte array to reduce array instantiation
+     * @param threeBytes the array to convert
+     * @param numSigBytes the number of significant bytes in your array
+     * @return four byte array in Base64 notation.
+     * @since 1.5.1
+     */
+    private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes )
+    {
+        encode3to4( threeBytes, 0, numSigBytes, b4, 0 );
+        return b4;
+    }   // end encode3to4
+
+    
+    /**
+     * Encodes up to three bytes of the array <var>source</var>
+     * and writes the resulting four Base64 bytes to <var>destination</var>.
+     * The source and destination arrays can be manipulated
+     * anywhere along their length by specifying 
+     * <var>srcOffset</var> and <var>destOffset</var>.
+     * This method does not check to make sure your arrays
+     * are large enough to accomodate <var>srcOffset</var> + 3 for
+     * the <var>source</var> array or <var>destOffset</var> + 4 for
+     * the <var>destination</var> array.
+     * The actual number of significant bytes in your array is
+     * given by <var>numSigBytes</var>.
+     *
+     * @param source the array to convert
+     * @param srcOffset the index where conversion begins
+     * @param numSigBytes the number of significant bytes in your array
+     * @param destination the array to hold the conversion
+     * @param destOffset the index where output will be put
+     * @return the <var>destination</var> array
+     * @since 1.3
+     */
+    private static byte[] encode3to4( 
+     byte[] source, int srcOffset, int numSigBytes,
+     byte[] destination, int destOffset )
+    {
+        //           1         2         3  
+        // 01234567890123456789012345678901 Bit position
+        // --------000000001111111122222222 Array position from threeBytes
+        // --------|    ||    ||    ||    | Six bit groups to index ALPHABET
+        //          >>18  >>12  >> 6  >> 0  Right shift necessary
+        //                0x3f  0x3f  0x3f  Additional AND
+        
+        // Create buffer with zero-padding if there are only one or two
+        // significant bytes passed in the array.
+        // We have to shift left 24 in order to flush out the 1's that appear
+        // when Java treats a value as negative that is cast from a byte to an int.
+        int inBuff =   ( numSigBytes > 0 ? ((source[ srcOffset     ] << 24) >>>  8) : 0 )
+                     | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 )
+                     | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 );
+
+        switch( numSigBytes )
+        {
+            case 3:
+                destination[ destOffset     ] = ALPHABET[ (inBuff >>> 18)        ];
+                destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+                destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>>  6) & 0x3f ];
+                destination[ destOffset + 3 ] = ALPHABET[ (inBuff       ) & 0x3f ];
+                return destination;
+                
+            case 2:
+                destination[ destOffset     ] = ALPHABET[ (inBuff >>> 18)        ];
+                destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+                destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>>  6) & 0x3f ];
+                destination[ destOffset + 3 ] = EQUALS_SIGN;
+                return destination;
+                
+            case 1:
+                destination[ destOffset     ] = ALPHABET[ (inBuff >>> 18)        ];
+                destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+                destination[ destOffset + 2 ] = EQUALS_SIGN;
+                destination[ destOffset + 3 ] = EQUALS_SIGN;
+                return destination;
+                
+            default:
+                return destination;
+        }   // end switch
+    }   // end encode3to4
+    
+    
+    
+    /**
+     * Serializes an object and returns the Base64-encoded
+     * version of that serialized object. If the object
+     * cannot be serialized or there is another error,
+     * the method will return <tt>null</tt>.
+     * The object is not GZip-compressed before being encoded.
+     *
+     * @param serializableObject The object to encode
+     * @return The Base64-encoded object
+     * @since 1.4
+     */
+    public static String encodeObject( java.io.Serializable serializableObject )
+    {
+        return encodeObject( serializableObject, NO_OPTIONS );
+    }   // end encodeObject
+    
+
+
+    /**
+     * Serializes an object and returns the Base64-encoded
+     * version of that serialized object. If the object
+     * cannot be serialized or there is another error,
+     * the method will return <tt>null</tt>.
+     * <p>
+     * Valid options:<pre>
+     *   GZIP: gzip-compresses object before encoding it.
+     *   DONT_BREAK_LINES: don't break lines at 76 characters
+     *     <i>Note: Technically, this makes your encoding non-compliant.</i>
+     * </pre>
+     * <p>
+     * Example: <code>encodeObject( myObj, Base64.GZIP )</code> or
+     * <p>
+     * Example: <code>encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
+     *
+     * @param serializableObject The object to encode
+     * @param options Specified options
+     * @return The Base64-encoded object
+     * @see Base64#GZIP
+     * @see Base64#DONT_BREAK_LINES
+     * @since 2.0
+     */
+    public static String encodeObject( java.io.Serializable serializableObject, int options )
+    {
+        // Streams
+        java.io.ByteArrayOutputStream  baos  = null; 
+        java.io.OutputStream           b64os = null; 
+        java.io.ObjectOutputStream     oos   = null; 
+        java.util.zip.GZIPOutputStream gzos  = null;
+        
+        // Isolate options
+        int gzip           = (options & GZIP);
+        int dontBreakLines = (options & DONT_BREAK_LINES);
+        
+        try
+        {
+            // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream
+            baos  = new java.io.ByteArrayOutputStream();
+            b64os = new Base64.OutputStream( baos, ENCODE | dontBreakLines );
+    
+            // GZip?
+            if( gzip == GZIP )
+            {
+                gzos = new java.util.zip.GZIPOutputStream( b64os );
+                oos  = new java.io.ObjectOutputStream( gzos );
+            }   // end if: gzip
+            else
+                oos   = new java.io.ObjectOutputStream( b64os );
+            
+            oos.writeObject( serializableObject );
+        }   // end try
+        catch( java.io.IOException e )
+        {
+            e.printStackTrace();
+            return null;
+        }   // end catch
+        finally
+        {
+            try{ oos.close();   } catch( Exception e ){}
+            try{ gzos.close();  } catch( Exception e ){}
+            try{ b64os.close(); } catch( Exception e ){}
+            try{ baos.close();  } catch( Exception e ){}
+        }   // end finally
+        
+        return new String( baos.toByteArray() );
+    }   // end encode
+    
+    
+
+    /**
+     * Encodes a byte array into Base64 notation.
+     * Does not GZip-compress data.
+     *
+     * @param source The data to convert
+     * @since 1.4
+     */
+    public static String encodeBytes( byte[] source )
+    {
+        return encodeBytes( source, 0, source.length, NO_OPTIONS );
+    }   // end encodeBytes
+
+    /**
+     * Added this variant of the method in so that RemoteGreenstoneServer.java
+     * can process large source and target filenames, without Base64 encoding 
+     * them introducing a new line.
+     * @param source The data to convert
+     * @since January
+     */
+    public static String encodeBytesInSingleLine( byte[] source)
+    {
+        return encodeBytes( source, 0, source.length, DONT_BREAK_LINES);
+    }   // end encodeBytes
+    
+    /**
+     * Encodes a byte array into Base64 notation.
+     * <p>
+     * Valid options:<pre>
+     *   GZIP: gzip-compresses object before encoding it.
+     *   DONT_BREAK_LINES: don't break lines at 76 characters
+     *     <i>Note: Technically, this makes your encoding non-compliant.</i>
+     * </pre>
+     * <p>
+     * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or
+     * <p>
+     * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
+     *
+     *
+     * @param source The data to convert
+     * @param options Specified options
+     * @see Base64#GZIP
+     * @see Base64#DONT_BREAK_LINES
+     * @since 2.0
+     */
+    public static String encodeBytes( byte[] source, int options )
+    {   
+        return encodeBytes( source, 0, source.length, options );
+    }   // end encodeBytes
+    
+    
+    /**
+     * Encodes a byte array into Base64 notation.
+     * Does not GZip-compress data.
+     *
+     * @param source The data to convert
+     * @param off Offset in array where conversion should begin
+     * @param len Length of data to convert
+     * @since 1.4
+     */
+    public static String encodeBytes( byte[] source, int off, int len )
+    {
+        return encodeBytes( source, off, len, NO_OPTIONS );
+    }   // end encodeBytes
+    
+    
+
+    /**
+     * Encodes a byte array into Base64 notation.
+     * <p>
+     * Valid options:<pre>
+     *   GZIP: gzip-compresses object before encoding it.
+     *   DONT_BREAK_LINES: don't break lines at 76 characters
+     *     <i>Note: Technically, this makes your encoding non-compliant.</i>
+     * </pre>
+     * <p>
+     * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or
+     * <p>
+     * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
+     *
+     *
+     * @param source The data to convert
+     * @param off Offset in array where conversion should begin
+     * @param len Length of data to convert
+     * @param options Specified options
+     * @see Base64#GZIP
+     * @see Base64#DONT_BREAK_LINES
+     * @since 2.0
+     */
+    public static String encodeBytes( byte[] source, int off, int len, int options )
+    {
+        // Isolate options
+        int dontBreakLines = ( options & DONT_BREAK_LINES );
+        int gzip           = ( options & GZIP   );
+        
+        // Compress?
+        if( gzip == GZIP )
+        {
+            java.io.ByteArrayOutputStream  baos  = null;
+            java.util.zip.GZIPOutputStream gzos  = null;
+            Base64.OutputStream            b64os = null;
+            
+    
+            try
+            {
+                // GZip -> Base64 -> ByteArray
+                baos = new java.io.ByteArrayOutputStream();
+                b64os = new Base64.OutputStream( baos, ENCODE | dontBreakLines );
+                gzos  = new java.util.zip.GZIPOutputStream( b64os ); 
+            
+                gzos.write( source, off, len );
+                gzos.close();
+            }   // end try
+            catch( java.io.IOException e )
+            {
+                e.printStackTrace();
+                return null;
+            }   // end catch
+            finally
+            {
+                try{ gzos.close();  } catch( Exception e ){}
+                try{ b64os.close(); } catch( Exception e ){}
+                try{ baos.close();  } catch( Exception e ){}
+            }   // end finally
+
+            return new String( baos.toByteArray() );
+        }   // end if: compress
+        
+        // Else, don't compress. Better not to use streams at all then.
+        else
+        {
+            // Convert option to boolean in way that code likes it.
+            boolean breakLines = dontBreakLines == 0;
+            
+            int    len43   = len * 4 / 3;
+            byte[] outBuff = new byte[   ( len43 )                      // Main 4:3
+                                       + ( (len % 3) > 0 ? 4 : 0 )      // Account for padding
+                                       + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines      
+            int d = 0;
+            int e = 0;
+            int len2 = len - 2;
+            int lineLength = 0;
+            for( ; d < len2; d+=3, e+=4 )
+            {
+                encode3to4( source, d+off, 3, outBuff, e );
+
+                lineLength += 4;
+                if( breakLines && lineLength == MAX_LINE_LENGTH )
+                {   
+                    outBuff[e+4] = NEW_LINE;
+                    e++;
+                    lineLength = 0;
+                }   // end if: end of line
+            }   // en dfor: each piece of array
+
+            if( d < len )
+            {
+                encode3to4( source, d+off, len - d, outBuff, e );
+                e += 4;
+            }   // end if: some padding needed
+
+            return new String( outBuff, 0, e );
+        }   // end else: don't compress
+        
+    }   // end encodeBytes
+    
+
+    
+    
+    
+/* ********  D E C O D I N G   M E T H O D S  ******** */
+    
+    
+    /**
+     * Decodes the first four bytes of array <var>fourBytes</var>
+     * and returns an array up to three bytes long with the
+     * decoded values.
+     *
+     * @param fourBytes the array with Base64 content
+     * @return array with decoded values
+     * @since 1.3
+     */
+    private static byte[] decode4to3( byte[] fourBytes )
+    {
+        byte[] outBuff1 = new byte[3];
+        int    count    = decode4to3( fourBytes, 0, outBuff1, 0 );
+        byte[] outBuff2 = new byte[ count ];
+        
+        for( int i = 0; i < count; i++ )
+            outBuff2[i] = outBuff1[i];
+        
+        return outBuff2;
+    }
+    
+    
+    
+    
+    /**
+     * Decodes four bytes from array <var>source</var>
+     * and writes the resulting bytes (up to three of them)
+     * to <var>destination</var>.
+     * The source and destination arrays can be manipulated
+     * anywhere along their length by specifying 
+     * <var>srcOffset</var> and <var>destOffset</var>.
+     * This method does not check to make sure your arrays
+     * are large enough to accomodate <var>srcOffset</var> + 4 for
+     * the <var>source</var> array or <var>destOffset</var> + 3 for
+     * the <var>destination</var> array.
+     * This method returns the actual number of bytes that 
+     * were converted from the Base64 encoding.
+     * 
+     *
+     * @param source the array to convert
+     * @param srcOffset the index where conversion begins
+     * @param destination the array to hold the conversion
+     * @param destOffset the index where output will be put
+     * @return the number of decoded bytes converted
+     * @since 1.3
+     */
+    private static int decode4to3( byte[] source, int srcOffset, byte[] destination, int destOffset )
+    {
+        // Example: Dk==
+        if( source[ srcOffset + 2] == EQUALS_SIGN )
+        {
+            // Two ways to do the same thing. Don't know which way I like best.
+            //int outBuff =   ( ( DECODABET[ source[ srcOffset    ] ] << 24 ) >>>  6 )
+            //              | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
+            int outBuff =   ( ( DECODABET[ source[ srcOffset    ] ] & 0xFF ) << 18 )
+                          | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 );
+            
+            destination[ destOffset ] = (byte)( outBuff >>> 16 );
+            return 1;
+        }
+        
+        // Example: DkL=
+        else if( source[ srcOffset + 3 ] == EQUALS_SIGN )
+        {
+            // Two ways to do the same thing. Don't know which way I like best.
+            //int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] << 24 ) >>>  6 )
+            //              | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
+            //              | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 );
+            int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] & 0xFF ) << 18 )
+                          | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
+                          | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) <<  6 );
+            
+            destination[ destOffset     ] = (byte)( outBuff >>> 16 );
+            destination[ destOffset + 1 ] = (byte)( outBuff >>>  8 );
+            return 2;
+        }
+        
+        // Example: DkLE
+        else
+        {
+            try{
+            // Two ways to do the same thing. Don't know which way I like best.
+            //int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] << 24 ) >>>  6 )
+            //              | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
+            //              | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 )
+            //              | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 );
+            int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] & 0xFF ) << 18 )
+                          | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
+                          | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) <<  6)
+                          | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF )      );
+
+            
+            destination[ destOffset     ] = (byte)( outBuff >> 16 );
+            destination[ destOffset + 1 ] = (byte)( outBuff >>  8 );
+            destination[ destOffset + 2 ] = (byte)( outBuff       );
+
+            return 3;
+            }catch( Exception e){
+                System.out.println(""+source[srcOffset]+ ": " + ( DECODABET[ source[ srcOffset     ] ]  ) );
+                System.out.println(""+source[srcOffset+1]+  ": " + ( DECODABET[ source[ srcOffset + 1 ] ]  ) );
+                System.out.println(""+source[srcOffset+2]+  ": " + ( DECODABET[ source[ srcOffset + 2 ] ]  ) );
+                System.out.println(""+source[srcOffset+3]+  ": " + ( DECODABET[ source[ srcOffset + 3 ] ]  ) );
+                return -1;
+            }   //e nd catch
+        }
+    }   // end decodeToBytes
+    
+    
+    
+    
+    /**
+     * Very low-level access to decoding ASCII characters in
+     * the form of a byte array. Does not support automatically
+     * gunzipping or any other "fancy" features.
+     *
+     * @param source The Base64 encoded data
+     * @param off    The offset of where to begin decoding
+     * @param len    The length of characters to decode
+     * @return decoded data
+     * @since 1.3
+     */
+    public static byte[] decode( byte[] source, int off, int len )
+    {
+        int    len34   = len * 3 / 4;
+        byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output
+        int    outBuffPosn = 0;
+        
+        byte[] b4        = new byte[4];
+        int    b4Posn    = 0;
+        int    i         = 0;
+        byte   sbiCrop   = 0;
+        byte   sbiDecode = 0;
+        for( i = off; i < off+len; i++ )
+        {
+            sbiCrop = (byte)(source[i] & 0x7f); // Only the low seven bits
+            sbiDecode = DECODABET[ sbiCrop ];
+            
+            if( sbiDecode >= WHITE_SPACE_ENC ) // White space, Equals sign or better
+            {
+                if( sbiDecode >= EQUALS_SIGN_ENC )
+                {
+                    b4[ b4Posn++ ] = sbiCrop;
+                    if( b4Posn > 3 )
+                    {
+                        outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn );
+                        b4Posn = 0;
+                        
+                        // If that was the equals sign, break out of 'for' loop
+                        if( sbiCrop == EQUALS_SIGN )
+                            break;
+                    }   // end if: quartet built
+                    
+                }   // end if: equals sign or better
+                
+            }   // end if: white space, equals sign or better
+            else
+            {
+                System.err.println( "Bad Base64 input character at " + i + ": " + source[i] + "(decimal)" );
+                return null;
+            }   // end else: 
+        }   // each input character
+                                   
+        byte[] out = new byte[ outBuffPosn ];
+        System.arraycopy( outBuff, 0, out, 0, outBuffPosn ); 
+        return out;
+    }   // end decode
+    
+    
+    
+    
+    /**
+     * Decodes data from Base64 notation, automatically
+     * detecting gzip-compressed data and decompressing it.
+     *
+     * @param s the string to decode
+     * @return the decoded data
+     * @since 1.4
+     */
+    public static byte[] decode( String s )
+    {   
+        byte[] bytes = s.getBytes();
+        bytes = decode( bytes, 0, bytes.length );
+        
+        // Check to see if it's gzip-compressed
+        // GZIP Magic Two-Byte Number: 0x8b1f (35615)
+        if( bytes.length >= 2 )
+        {
+            
+            int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);       
+            if(
+            bytes != null &&      // In case decoding returned null
+            bytes.length >= 4  && // Don't want to get ArrayIndexOutOfBounds exception
+            java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) 
+            {
+                java.io.ByteArrayInputStream  bais = null;
+                java.util.zip.GZIPInputStream gzis = null;
+                java.io.ByteArrayOutputStream baos = null;
+                byte[] buffer = new byte[2048];
+                int    length = 0;
+
+                try
+                {
+                    baos = new java.io.ByteArrayOutputStream();
+                    bais = new java.io.ByteArrayInputStream( bytes );
+                    gzis = new java.util.zip.GZIPInputStream( bais );
+
+                    while( ( length = gzis.read( buffer ) ) >= 0 )
+                    {
+                        baos.write(buffer,0,length);
+                    }   // end while: reading input
+
+                    // No error? Get new bytes.
+                    bytes = baos.toByteArray();
+
+                }   // end try
+                catch( java.io.IOException e )
+                {
+                    // Just return originally-decoded bytes
+                }   // end catch
+                finally
+                {
+                    try{ baos.close(); } catch( Exception e ){}
+                    try{ gzis.close(); } catch( Exception e ){}
+                    try{ bais.close(); } catch( Exception e ){}
+                }   // end finally
+
+            }   // end if: gzipped
+        }   // end if: bytes.length >= 2
+        
+        return bytes;
+    }   // end decode
+
+
+    
+
+    /**
+     * Attempts to decode Base64 data and deserialize a Java
+     * Object within. Returns <tt>null</tt> if there was an error.
+     *
+     * @param encodedObject The Base64 data to decode
+     * @return The decoded and deserialized object
+     * @since 1.5
+     */
+    public static Object decodeToObject( String encodedObject )
+    {
+        // Decode and gunzip if necessary
+        byte[] objBytes = decode( encodedObject );
+        
+        java.io.ByteArrayInputStream  bais = null;
+        java.io.ObjectInputStream     ois  = null;
+        Object obj = null;
+        
+        try
+        {
+            bais = new java.io.ByteArrayInputStream( objBytes );
+            ois  = new java.io.ObjectInputStream( bais );
+        
+            obj = ois.readObject();
+        }   // end try
+        catch( java.io.IOException e )
+        {
+            e.printStackTrace();
+            obj = null;
+        }   // end catch
+        catch( java.lang.ClassNotFoundException e )
+        {
+            e.printStackTrace();
+            obj = null;
+        }   // end catch
+        finally
+        {
+            try{ bais.close(); } catch( Exception e ){}
+            try{ ois.close();  } catch( Exception e ){}
+        }   // end finally
+        
+        return obj;
+    }   // end decodeObject
+    
+    
+    /* ********  I N N E R   C L A S S   I N P U T S T R E A M  ******** */
+    
+    
+    
+    /**
+     * It will read data from another
+     * {@link java.io.InputStream}, given in the constructor,
+     * and encode/decode to/from Base64 notation on the fly.
+     *
+     * @see Base64
+     * @see java.io.FilterInputStream
+     * @since 1.3
+     */
+    public static class InputStream extends java.io.FilterInputStream
+    {
+        private int     options;        // Options specified
+        private boolean encode;         // Encoding or decoding
+        private int     position;       // Current position in the buffer
+        private byte[]  buffer;         // Small buffer holding converted data
+        private int     bufferLength;   // Length of buffer (3 or 4)
+        private int     numSigBytes;    // Number of meaningful bytes in the buffer
+        private int     lineLength;
+        private boolean breakLines;     // Break lines at less than 80 characters
+        
+        
+        /**
+         * Constructs a InputStream in DECODE mode.
+         *
+         * @param in the {@link java.io.InputStream} from which to read data.
+         * @since 1.3
+         */
+        public InputStream( java.io.InputStream in )
+        {   
+            this( in, DECODE );
+        }   // end constructor
+        
+        
+        /**
+         * Constructs a InputStream in
+         * either ENCODE or DECODE mode.
+         * <p>
+         * Valid options:<pre>
+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DONT_BREAK_LINES: don't break lines at 76 characters
+         *     (only meaningful when encoding)
+         *     <i>Note: Technically, this makes your encoding non-compliant.</i>
+         * </pre>
+         * <p>
+         * Example: <code>new Base64.InputStream( in, Base64.DECODE )</code>
+         *
+         *
+         * @param in the {@link java.io.InputStream} from which to read data.
+         * @param options Specified options
+         * @see Base64#ENCODE
+         * @see Base64#DECODE
+         * @see Base64#DONT_BREAK_LINES
+         * @since 2.0
+         */
+        public InputStream( java.io.InputStream in, int options )
+        {   
+            super( in );
+            this.options      = options;
+            this.breakLines   = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES;
+            this.encode       = (options & ENCODE) == ENCODE;
+            this.bufferLength = encode ? 4 : 3;
+            this.buffer   = new byte[ bufferLength ];
+            this.position = -1;
+            this.lineLength = 0;
+        }   // end constructor
+        
+        /**
+         * Reads enough of the input stream to convert
+         * to/from Base64 and returns the next byte.
+         *
+         * @return next byte
+         * @since 1.3
+         */
+        public int read() throws java.io.IOException 
+        { 
+            // Do we need to get data?
+            if( position < 0 )
+            {
+                if( encode )
+                {
+                    byte[] b3 = new byte[3];
+                    int numBinaryBytes = 0;
+                    for( int i = 0; i < 3; i++ )
+                    {
+                        try
+                        { 
+                            int b = in.read();
+                            
+                            // If end of stream, b is -1.
+                            if( b >= 0 )
+                            {
+                                b3[i] = (byte)b;
+                                numBinaryBytes++;
+                            }   // end if: not end of stream
+                            
+                        }   // end try: read
+                        catch( java.io.IOException e )
+                        {   
+                            // Only a problem if we got no data at all.
+                            if( i == 0 )
+                                throw e;
+                            
+                        }   // end catch
+                    }   // end for: each needed input byte
+                    
+                    if( numBinaryBytes > 0 )
+                    {
+                        encode3to4( b3, 0, numBinaryBytes, buffer, 0 );
+                        position = 0;
+                        numSigBytes = 4;
+                    }   // end if: got data
+                    else
+                    {
+                        return -1;
+                    }   // end else
+                }   // end if: encoding
+                
+                // Else decoding
+                else
+                {
+                    byte[] b4 = new byte[4];
+                    int i = 0;
+                    for( i = 0; i < 4; i++ )
+                    {
+                        // Read four "meaningful" bytes:
+                        int b = 0;
+                        do{ b = in.read(); }
+                        while( b >= 0 && DECODABET[ b & 0x7f ] <= WHITE_SPACE_ENC );
+                        
+                        if( b < 0 )
+                            break; // Reads a -1 if end of stream
+                        
+                        b4[i] = (byte)b;
+                    }   // end for: each needed input byte
+                    
+                    if( i == 4 )
+                    {
+                        numSigBytes = decode4to3( b4, 0, buffer, 0 );
+                        position = 0;
+                    }   // end if: got four characters
+                    else if( i == 0 ){
+                        return -1;
+                    }   // end else if: also padded correctly
+                    else
+                    {
+                        // Must have broken out from above.
+                        throw new java.io.IOException( "Improperly padded Base64 input." );
+                    }   // end 
+                    
+                }   // end else: decode
+            }   // end else: get data
+            
+            // Got data?
+            if( position >= 0 )
+            {
+                // End of relevant data?
+                if( /*!encode &&*/ position >= numSigBytes )
+                    return -1;
+                
+                if( encode && breakLines && lineLength >= MAX_LINE_LENGTH )
+                {
+                    lineLength = 0;
+                    return '\n';
+                }   // end if
+                else
+                {
+                    lineLength++;   // This isn't important when decoding
+                                    // but throwing an extra "if" seems
+                                    // just as wasteful.
+                    
+                    int b = buffer[ position++ ];
+
+                    if( position >= bufferLength )
+                        position = -1;
+
+                    return b & 0xFF; // This is how you "cast" a byte that's
+                                     // intended to be unsigned.
+                }   // end else
+            }   // end if: position >= 0
+            
+            // Else error
+            else
+            {   
+                // When JDK1.4 is more accepted, use an assertion here.
+                throw new java.io.IOException( "Error in Base64 code reading stream." );
+            }   // end else
+        }   // end read
+        
+        
+        /**
+         * Calls {@link #read} repeatedly until the end of stream
+         * is reached or <var>len</var> bytes are read.
+         * Returns number of bytes read into array or -1 if
+         * end of stream is encountered.
+         *
+         * @param dest array to hold values
+         * @param off offset for array
+         * @param len max number of bytes to read into array
+         * @return bytes read into array or -1 if end of stream is encountered.
+         * @since 1.3
+         */
+        public int read( byte[] dest, int off, int len ) throws java.io.IOException
+        {
+            int i;
+            int b;
+            for( i = 0; i < len; i++ )
+            {
+                b = read();
+                
+                //if( b < 0 && i == 0 )
+                //    return -1;
+                
+                if( b >= 0 )
+                    dest[off + i] = (byte)b;
+                else if( i == 0 )
+                    return -1;
+                else
+                    break; // Out of 'for' loop
+            }   // end for: each byte read
+            return i;
+        }   // end read
+        
+    }   // end inner class InputStream
+    
+    
+    
+    
+    
+    
+    /* ********  I N N E R   C L A S S   O U T P U T S T R E A M  ******** */
+    
+    
+    
+    /**
+     * A OutputStream will write data to another
+     * {@link java.io.OutputStream}, given in the constructor,
+     * and encode/decode to/from Base64 notation on the fly.
+     *
+     * @see Base64
+     * @see java.io.FilterOutputStream
+     * @since 1.3
+     */
+    public static class OutputStream extends java.io.FilterOutputStream
+    {
+        private int     options;
+        private boolean encode;
+        private int     position;
+        private byte[]  buffer;
+        private int     bufferLength;
+        private int     lineLength;
+        private boolean breakLines;
+        private byte[]  b4; // Scratch used in a few places
+        private boolean suspendEncoding;
+        
+        /**
+         * Constructs a OutputStream in ENCODE mode.
+         *
+         * @param out the {@link java.io.OutputStream} to which data will be written.
+         * @since 1.3
+         */
+        public OutputStream( java.io.OutputStream out )
+        {   
+            this( out, ENCODE );
+        }   // end constructor
+        
+        
+        /**
+         * Constructs a OutputStream in
+         * either ENCODE or DECODE mode.
+         * <p>
+         * Valid options:<pre>
+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DONT_BREAK_LINES: don't break lines at 76 characters
+         *     (only meaningful when encoding)
+         *     <i>Note: Technically, this makes your encoding non-compliant.</i>
+         * </pre>
+         * <p>
+         * Example: <code>new Base64.OutputStream( out, Base64.ENCODE )</code>
+         *
+         * @param out the {@link java.io.OutputStream} to which data will be written.
+         * @param options Specified options.
+         * @see Base64#ENCODE
+         * @see Base64#DECODE
+         * @see Base64#DONT_BREAK_LINES
+         * @since 1.3
+         */
+        public OutputStream( java.io.OutputStream out, int options )
+        {   
+            super( out );
+            this.options      = options;
+            this.breakLines   = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES;
+            this.encode       = (options & ENCODE) == ENCODE;
+            this.bufferLength = encode ? 3 : 4;
+            this.buffer       = new byte[ bufferLength ];
+            this.position     = 0;
+            this.lineLength   = 0;
+            this.suspendEncoding = false;
+            this.b4           = new byte[4];
+        }   // end constructor
+        
+        
+        /**
+         * Writes the byte to the output stream after
+         * converting to/from Base64 notation.
+         * When encoding, bytes are buffered three
+         * at a time before the output stream actually
+         * gets a write() call.
+         * When decoding, bytes are buffered four
+         * at a time.
+         *
+         * @param theByte the byte to write
+         * @since 1.3
+         */
+        public void write(int theByte) throws java.io.IOException
+        {
+            // Encoding suspended?
+            if( suspendEncoding )
+            {
+                super.out.write( theByte );
+                return;
+            }   // end if: supsended
+            
+            // Encode?
+            if( encode )
+            {
+                buffer[ position++ ] = (byte)theByte;
+                if( position >= bufferLength )  // Enough to encode.
+                {
+                    out.write( encode3to4( b4, buffer, bufferLength ) );
+
+                    lineLength += 4;
+                    if( breakLines && lineLength >= MAX_LINE_LENGTH )
+                    {
+                        out.write( NEW_LINE );
+                        lineLength = 0;
+                    }   // end if: end of line
+
+                    position = 0;
+                }   // end if: enough to output
+            }   // end if: encoding
+
+            // Else, Decoding
+            else
+            {
+                // Meaningful Base64 character?
+                if( DECODABET[ theByte & 0x7f ] > WHITE_SPACE_ENC )
+                {
+                    buffer[ position++ ] = (byte)theByte;
+                    if( position >= bufferLength )  // Enough to output.
+                    {
+                        int len = Base64.decode4to3( buffer, 0, b4, 0 );
+                        out.write( b4, 0, len );
+                        //out.write( Base64.decode4to3( buffer ) );
+                        position = 0;
+                    }   // end if: enough to output
+                }   // end if: meaningful base64 character
+                else if( DECODABET[ theByte & 0x7f ] != WHITE_SPACE_ENC )
+                {
+                    throw new java.io.IOException( "Invalid character in Base64 data." );
+                }   // end else: not white space either
+            }   // end else: decoding
+        }   // end write
+        
+        
+        
+        /**
+         * Calls {@link #write} repeatedly until <var>len</var> 
+         * bytes are written.
+         *
+         * @param theBytes array from which to read bytes
+         * @param off offset for array
+         * @param len max number of bytes to read into array
+         * @since 1.3
+         */
+        public void write( byte[] theBytes, int off, int len ) throws java.io.IOException
+        {
+            // Encoding suspended?
+            if( suspendEncoding )
+            {
+                super.out.write( theBytes, off, len );
+                return;
+            }   // end if: supsended
+            
+            for( int i = 0; i < len; i++ )
+            {
+                write( theBytes[ off + i ] );
+            }   // end for: each byte written
+            
+        }   // end write
+        
+        
+        
+        /**
+         * Method added by PHIL. [Thanks, PHIL. -Rob]
+         * This pads the buffer without closing the stream.
+         */
+        public void flushBase64() throws java.io.IOException 
+        {
+            if( position > 0 )
+            {
+                if( encode )
+                {
+                    out.write( encode3to4( b4, buffer, position ) );
+                    position = 0;
+                }   // end if: encoding
+                else
+                {
+                    throw new java.io.IOException( "Base64 input not properly padded." );
+                }   // end else: decoding
+            }   // end if: buffer partially full
+
+        }   // end flush
+
+        
+        /** 
+         * Flushes and closes (I think, in the superclass) the stream. 
+         *
+         * @since 1.3
+         */
+        public void close() throws java.io.IOException
+        {
+            // 1. Ensure that pending characters are written
+            flushBase64();
+
+            // 2. Actually close the stream
+            // Base class both flushes and closes.
+            super.close();
+            
+            buffer = null;
+            out    = null;
+        }   // end close
+        
+        
+        
+        /**
+         * Suspends encoding of the stream.
+         * May be helpful if you need to embed a piece of
+         * base640-encoded data in a stream.
+         *
+         * @since 1.5.1
+         */
+        public void suspendEncoding() throws java.io.IOException 
+        {
+            flushBase64();
+            this.suspendEncoding = true;
+        }   // end suspendEncoding
+        
+        
+        /**
+         * Resumes encoding of the stream.
+         * May be helpful if you need to embed a piece of
+         * base640-encoded data in a stream.
+         *
+         * @since 1.5.1
+         */
+        public void resumeEncoding()
+        {
+            this.suspendEncoding = false;
+        }   // end resumeEncoding
+        
+        
+        
+    }   // end inner class OutputStream
+    
+    
+}   // end class Base64
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/CombineGraphs.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/CombineGraphs.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/CombineGraphs.java	(revision 31635)
@@ -0,0 +1,138 @@
+/** This class is not currently used and therefore will not be compiled by makegli. */
+
+package org.greenstone.gatherer.feedback;
+
+import java.io.*;
+import javax.imageio.*;
+import java.awt.*;
+import java.awt.image.*;
+import com.sun.image.codec.jpeg.*;
+import javax.swing.*;
+
+/**
+ *  CombineGraphs class is a class to combine 2 images file into 1 jpeg file
+ *  called test.jpg using point by point comparison.
+ *  <p>User need to supplied the valid exist filenames of the 2 images that 
+ *  they want to combine.If in any one point in 2nd image file is not transparent
+ *  then that point will be drawn to the 1st image file.</p>  
+ *  <p>To run this class properly the format is :
+ *  <br><i>java CombineGraphs filename1 filename2</i>
+ *  <br>where filename1 is the names of the first image file.
+ *  <br>and filename2 is the names of the second image file. This file must
+ *  contains transparent.</p>
+ *  <p>For example: If user wants to combine the screenshot image file with the
+ *  image file of the scribble lines, then
+ *  <br>filename1 will be the filename of the screenshot image file, and
+ *  <br>filename2 will be the filename of the scribble lines image file.</p>
+ *
+ *  @author Veronica Liesaputra
+ */
+
+public class CombineGraphs extends JPanel
+{
+    /**
+     * This is a variable to stored the image that is stored inside f1.
+     * After the combine method called this variable will store the resulted image of
+     * combining 2 images together.
+     */
+    private BufferedImage bi1;
+
+    /**
+     * This is a variable to stored the image that is stored inside f2.
+     */
+    private BufferedImage bi2;
+
+    /**
+     * This is a variable to stored the first image file that the user supplied.
+     */
+    private File f1;
+
+    /**
+     * This is a variable to stored the second image file that the user supplied.
+     */
+    private File f2;
+    
+    /**
+     * It will read the image files user supplied and stored the image
+     * to the appropriate bufferedimage variable.
+     * (Precondition: (file1 != null) && (file2 != null))
+     * @param file1   the name of the first image file.
+     * @param file2   the name of the second image file.
+     */
+    public CombineGraphs(String file1,String file2)
+    {
+	try
+	    {
+		f1 = new File(file1);
+		f2 = new File(file2);
+		bi1 = ImageIO.read(f1);
+		bi2 = ImageIO.read(f2);	
+	    }
+	catch(IOException exp) {}
+    }   
+
+    /**
+     * This method will combine the 2 buffered images into 1 buffered image and 
+     * saved it to an output file in jpeg format.
+     * (Precondition: (filename != null))
+     * @param filename the name of the output file.
+     * (Postcondition: the combined image is stored in a jpeg file with the name
+     *                 as specified in filename.)
+     */
+    public void combine(String filename)
+    {
+
+	/*using bi1 as the base layer that we use to compare point by point with bi2. 
+	  if in one point in bi2 its not transparent then we draw that point to bi1.*/ 
+	Graphics2D G1;
+	
+	G1 = bi1.createGraphics();
+	
+	int width = bi2.getWidth();
+	int height = bi2.getHeight();
+
+	for (int y = 0 ; y < height ; y++) 
+	    {
+		for (int x = 0 ; x < width ; x++)
+		    {
+			int rgb = bi2.getRGB(x,y);
+			if (rgb != 0)
+			    {
+				G1.setColor(new Color(rgb,true));
+				G1.drawLine(x,y,x,y);
+			    }
+		    }
+	    }
+	
+	// Save the combined bufferedimage into a jpeg output file.
+	try {
+	    File file = new File(filename);
+	    FileOutputStream out = new FileOutputStream(file);
+	    
+	    JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
+	    JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(bi1);
+	    param.setQuality(1.0f, false);
+	    encoder.setJPEGEncodeParam(param);
+	    encoder.encode(bi1);
+	    
+	} catch (Exception ex) {}
+    }
+
+    /**
+     * This the main method that need 2 inputs from the user for it to
+     * work properly. 
+     * The inputs are the 2 image filenames that user want to combine.
+     */
+    public static void main (String[] args)
+    {
+	CombineGraphs cb = new CombineGraphs(args[0],args[1]);
+	cb.combine("test.jpg");
+    }
+}
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/CompGroup.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/CompGroup.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/CompGroup.java	(revision 31635)
@@ -0,0 +1,60 @@
+package org.greenstone.gatherer.feedback;
+
+import java.io.*;
+
+/**
+ * The instance of this class will hold a UserComponent and the status of the UserComponent.
+ * For each of the CompGroup instance will contains all the information and the status of
+ * a window that is opened by the application the user used.
+ * @author Veronica Liesaputra
+ */
+public class CompGroup implements Serializable
+{
+    /**
+     *  This variable will hold the status of the window that is opened by the application.
+     */
+    private String comp_status;
+
+    /**
+     *  This variable will hold the information about the window that is opened by the
+     *  application.
+     */
+    private UserComponent comp;
+
+    public CompGroup ()
+    {
+
+    }
+    
+    /**
+     *  It will make CompGroup instance that hold the information and status of the window
+     *  thats is opened by the application.
+     *  @param curr_comp  information about the window opened by the application.
+     *  @param curr_status status of the window opened by the application.
+     */
+    public CompGroup (UserComponent curr_comp,String curr_status)
+    {
+	comp_status = curr_status;
+	comp = curr_comp;
+    }
+    
+    /**
+     * This method will return the status of the window opened by the application.
+     * @return the status of the window.
+     */
+    public String getStatus ()
+    {
+	return comp_status;
+    }
+    
+    /**
+     * This method will return the information about the window opened by the application.
+     * @return the information about the window.
+     */
+    public UserComponent getComponent()
+    {
+	return comp;
+    }
+}
+
+  
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/CompListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/CompListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/CompListener.java	(revision 31635)
@@ -0,0 +1,944 @@
+package org.greenstone.gatherer.feedback;
+
+import org.omg.IOP.ComponentIdHelper;
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.tree.*;
+import javax.swing.table.*;
+import javax.swing.text.*;
+import java.awt.image.*;
+import javax.imageio.ImageIO;
+import javax.swing.plaf.*;
+import javax.swing.plaf.basic.*;
+import javax.swing.colorchooser.*;
+import java.awt.event.*;
+import javax.swing.event.*;
+import java.text.MessageFormat;
+import javax.swing.plaf.metal.MetalIconFactory.*;
+import javax.swing.plaf.metal.MetalIconFactory;
+import javax.swing.plaf.basic.BasicInternalFrameTitlePane;
+import javax.swing.plaf.metal.MetalInternalFrameTitlePane;
+
+/**
+ *  In general, this class will get add special listener 
+ *  to a Container and of all the components inside it.
+ *  <p>This class will also add special listener to the Container and each of the components inside 
+ *  the Container. So, it will allows special listener  to record and listen each of the actions 
+ *  the user do to the Container or any of the components inside it. The special listener
+ *  is an instance of ActionRecorderDialog.</p>
+ *  <p>This class will only recognized all the components that is belongs to the javax.swing package
+ *  which are all the implementing classes of RootPaneContainer interface and all the direct known
+ *  subclasses of JComponent. All the other components will be stated simply as Container 
+ *  and user cannot really get much information out of it.</p>
+ *  <p>The added special listener to this component is as what its allowed in API, if the component
+ *  have some modified listener added to it then that action will not be known and recorded by
+ *  the special listener.</p>
+ *  
+ *  @author Veronica Liesaputra
+ */
+public class CompListener
+{
+    /**
+     *  This variable will hold the special listener that will listen and record any of the 
+     *  actions user do to the Container and any of the components inside it.
+     */
+    private ActionRecorderDialog m;
+   
+    /**
+     *  This variable will hold the pair of the component and the model of the component.
+     *  With this variable allows ActionRecorderDialog listener to know which component the model that fires an action
+     *  belongs to. In this HashMap the model is the key and the component where the model belongs to is
+     *  the value. 
+     */
+    private HashMap hash;
+    
+    /**
+     *  This constructor will set the special listener, the locale and the hashmap to be used in
+     *  this Container.
+     *  (Precondition: dialog != null)
+     *  @param dialog  its the special listener that will listen and record all the action
+     *                 user do to any of the component inside the Container.
+     *  @param h       its the HashMap that will hold the pair of model with the component it belongs.
+     */
+    public CompListener 
+	(ActionRecorderDialog dialog,HashMap h)
+    {
+	m = dialog;
+	hash = h;
+    }
+	
+    /**
+     *  If the JTextComponent is a JTextField then it will add the special action listener to it.
+     *  (Precondition: txt != null)
+     *  @param txt its the JTextComponent we want to get all the information from.
+     */
+    public void getTextAreaInfo(JTextComponent txt)
+    {
+	if (txt instanceof JTextField)
+	    {
+		((JTextField)txt).removeActionListener(m);
+		((JTextField)txt).addActionListener(m);
+	    }
+    }
+
+    /**
+     *  This method will add the special action listener to the JButton.
+     *  (Precondition: button != null)
+     *  @param button its the JButton we want to get the information from.
+     */
+    public void getButtonInfo (JButton button)
+    {
+	button.removeActionListener(m);
+	button.addActionListener(m);
+    }
+
+     /**
+     *  This method will get add the special change listener to the JProgressBar.
+     *  (Precondition: bar != null)
+     *  @param  bar   its the JProgressBar we want to get information from.
+     */
+    public void getProgressBarInfo (JProgressBar bar)
+    {
+	bar.removeChangeListener(m);
+	bar.addChangeListener(m);
+    }
+    
+    /**
+     *  This method will add the special change listener to the JSlider.
+     *  (Precondition: slider != null)
+     *  @param  slider its the JSlider we want to get information from.
+     */
+    public void getSliderInfo (JSlider slider)
+    {
+	slider.removeChangeListener(m);
+	slider.addChangeListener(m);
+    }
+
+    /**
+     *  This method will add the special change listener to the JSpinner.
+     *  (Precondition: spinner != null)
+     *  @param spinner its the JSpinner we want to the information from.
+     */
+    public void getSpinnerInfo (JSpinner spinner)
+    {
+	spinner.removeChangeListener(m);
+	spinner.addChangeListener(m);
+    }
+    
+    /**
+     * Dummy method to get JLabel.
+     */
+    public void getLabelInfo(JLabel lbl)
+    {}
+    
+    /**
+     *  This method will add the special action listener to the JComboBox.
+     *  (Precondition: box != null)
+     *  @param box    its the JComboBox we want to get information from.
+     */
+    public void getComboBoxInfo(JComboBox box)
+    {
+	ComboBoxModel cm;
+	cm = box.getModel();
+
+	box.removeActionListener(m);
+	box.addActionListener(m);
+    }
+
+    /**
+     *  This method will add the special action listener to the JToggleButton.
+     *  (Precondition: button != null)
+     *  @param button its a JToggleButton inside the window.
+     */
+    public void getToggleButtonInfo (JToggleButton button)
+    {
+	button.removeActionListener(m);
+	button.addActionListener(m);
+    }
+
+    /**
+     *  This method will add the special action listener to the JMenuItem.
+     *  (Precondition: menuitem != null)
+     *  @param menuitem its the JMenuItem we want to get the information from.
+     */
+    public void getMenuItemInfo (JMenuItem menuitem)
+    {
+	menuitem.removeActionListener(m);
+	menuitem.addActionListener(m);
+    }
+
+    /**
+     *  This method will get all the components inside JMenuBar.
+     *  (Precondition: menubar != null)
+     *  @param menubar its the JMenuBar we want to get the information from.
+     */
+    public void getMenuBarInfo (JMenuBar menubar)
+    {
+	int i;
+	for (i= 0 ; i < menubar.getMenuCount() ; i++)
+	    {
+		getInside(menubar.getMenu(i));
+	    }
+    }
+
+    /**
+     *  This method will add the special action and menu listener to the JMenu and
+     *  get the components inside JMenu.
+     *  (Precondition: menu != null)
+     *  @param menu   its the JMenu we want to the the information from.
+     */
+    public void getMenuInfo (JMenu menu)
+    {
+	menu.removeActionListener(m);
+	menu.removeMenuListener(m);
+	menu.addActionListener(m);
+	menu.addMenuListener(m);
+
+	int i;
+	for (i= 0 ; i < menu.getMenuComponentCount() ; i++)
+	    {
+		getInside((Container)menu.getMenuComponent(i));
+	    }
+    }
+    
+    /**
+     *  This method will get all the components inside the JApplet.
+     *  (Precondition: applet != null)
+     *  @param applet its the JApplet we want to get the information from.
+     */
+    public void getAppletInfo (JApplet applet)
+    {
+	Component[] group;
+	group = applet.getComponents();
+	int i;
+
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)applet.getComponent(i));
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get all the components inside the JWindow.
+     *  (Precondition: window != null)
+     *  @param window its the JWindow we want to get the information from.
+     */
+    public void getWindowInfo (JWindow window)
+    {
+	Component[] group;
+	group = window.getComponents();
+	int i;
+
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)window.getComponent(i));
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get all the components inside the JDialog.
+     *  (Precondition: dialog != null)
+     *  @param dialog its the JDialog we want to get the information from.
+     */
+    public void getDialogInfo (JDialog dialog)
+    {
+	Component[] group;
+	group = dialog.getComponents();
+	int i;
+
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)dialog.getComponent(i));
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get all the components inside the JInternalFrame.
+     *  (Precondition: frame != null)
+     *  @param frame  its the JInternalFrame we want to get the information from.
+     */
+    public void getInternalFrameInfo (JInternalFrame frame)
+    {
+	Component[] group;
+	group = frame.getComponents();
+	int i;
+
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)frame.getComponent(i));
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get all the components inside the JFrame.
+     *  (Precondition: frame != null)
+     *  @param frame  its the JFrame we want to get the information from.
+     */
+    public void getFrameInfo (JFrame frame)
+    {
+	Component[] group;
+	group = frame.getComponents();
+	int i;
+	
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)frame.getComponent(i));
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get all the components inside the JPanel.
+     *  (Precondition: pane != null)
+     *  @param pane   its the JPanel we want to get the information from.
+     */
+    public void getPanelInfo (JPanel pane)
+    {
+	Component[] group;
+	group = pane.getComponents();
+	int i;
+
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)pane.getComponent(i));
+		    }
+	    }
+    }
+    
+    /**
+     *  This method will get all the components inside the JScrollPane.
+     *  (Precondition: scroll != null)
+     *  @param scroll its the JScrollPane we want to get the information from.
+     */
+    public void getScrollInfo (JScrollPane scroll)
+    {
+	Component[] group;
+	group = scroll.getComponents();
+	int i;
+	
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)group[i]);
+		    }
+	    }
+    }
+
+    /**
+     *  This method will add special change listener to the JTabbedPane and get all
+     *  the components inside it.
+     *  (Precondition: tab != null)
+     *  @param tab   its the JTabbedPane we want to get the information from.
+     */
+    public void getTabbedPaneInfo (JTabbedPane tab)
+    {
+	int j;
+	
+	tab.removeChangeListener(m);
+	tab.addChangeListener(m);
+
+	for (j = 0 ; j < tab.getTabCount(); j++)
+	    {	
+		
+		Component group = tab.getComponentAt(j);
+		getInside((Container)group);
+		
+	    }
+    }
+    
+    /**
+     *  This is the dummy method to ge the option pane info.
+     */
+    public void getOptionPaneInfo (JOptionPane op)
+    {}
+
+    /**
+     *  This method will get components inside the JViewPort.
+     *  (Precondition: vp != null)
+     *  @param vp   its the JViewPort we want to get the information from.
+     */
+    public void getViewPortInfo (JViewport vp)
+    {
+	Component[] group;
+	group = vp.getComponents();
+	int i;
+
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)group[i]);
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get components inside the Box.
+     *  (Precondition: box != null)
+     *  @param box  its the Box we want to get the information from.
+     */
+    public void getBoxInfo (Box box)
+    {
+	Component[] group;
+	group = box.getComponents();
+	int i;
+
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)group[i]);
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get components inside the JSplitPane.
+     *  (Precondition: split != null)
+     *  @param split  its the JSplitPane we want to get the information from.
+     */
+    public void getSplitPaneInfo (JSplitPane split)
+    {
+	Component[] group;
+	group = split.getComponents();
+	int i;
+
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)group[i]);
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get add special list selection listener to the JList.
+     *  (Precondition: list != null)
+     *  @param list  its the JList we want to get the information from.
+     */
+    public void getListInfo (JList list)
+    {
+	ListModel ls;
+	ls = list.getModel();
+	
+	list.removeListSelectionListener(m);
+	list.addListSelectionListener(m);
+	
+	if (ls != null)
+	    {
+		if (hash.containsKey(ls) == false)
+		    hash.put(ls,list);
+		ls.removeListDataListener(m);
+		ls.addListDataListener(m);
+	    }
+    }
+
+    /**
+     *  This method will add special change listener to the JColorChooser.
+     *  (Precondition: color != null)
+     *  @param color  its the JColorChooser we want to get the information from.
+     */
+    public void getColorInfo (JColorChooser color)
+    {
+	ColorSelectionModel cm;
+	cm = color.getSelectionModel();
+	
+	if (cm != null)
+	    {
+		if (hash.containsKey(cm) == false)
+		    hash.put(cm,color);
+		cm.removeChangeListener(m);
+		cm.addChangeListener(m);
+	    }
+
+	getInside((Container) color.getPreviewPanel());
+    }
+
+    /**
+     *  This method will add special action listener to the JFileChooser.
+     *  (Precondition: fc != null)
+     *  @param fc  its the JFileChooser we want to get the information from.
+     */
+    public void getFileChooserInfo (JFileChooser fc)
+    {
+	fc.removeActionListener(m);
+	fc.addActionListener(m);
+    }		
+
+    /**
+     *  This method will add special list selection and column model listener 
+     *  to the JTable.
+     *  (Precondition: table != null)
+     *  @param table  its the JTable we want to get the information from.
+     */
+    public void getTableInfo (JTable table)
+    {
+	TableModel tm;
+	tm = table.getModel();
+	
+	TableColumnModel tcm;
+	tcm = table.getColumnModel();
+	ListSelectionModel lm;
+	lm = table.getSelectionModel(); 
+
+	if (lm != null)
+	    {
+		if (hash.containsKey(lm) == false)
+		    hash.put(lm,table);
+		lm.removeListSelectionListener(m);
+		lm.addListSelectionListener(m);
+	    }
+	
+	if (tcm != null)
+	    {
+		if (hash.containsKey(tcm) == false)
+		    hash.put(tcm,table);
+		tcm.removeColumnModelListener(m);
+		tcm.addColumnModelListener(m);
+	    }
+    }
+
+    /**
+     *  This is a dummy method to get the JScrollBar.
+     */
+    public void getScrollBarInfo (JScrollBar scroll)
+    {
+    }
+
+    /**
+     *  This method will add special tree selection listener to the JTree.
+     *  (Precondition: tree != null)
+     *  @param tree  its the JTree we want to get the information from.
+     */
+    public void getTreeInfo (JTree tree)
+    {
+	TreeModel tm;
+	tm = tree.getModel();
+	
+	tree.removeTreeSelectionListener(m);
+	tree.addTreeSelectionListener(m);
+	
+	if (tm != null)
+	    {
+		if (hash.containsKey(tm) == false)
+		    hash.put(tm,tree);
+		tm.removeTreeModelListener(m);
+		tm.addTreeModelListener(m);
+	    }
+    }
+       
+    /**
+     *  This method is a dummy method to get Container.
+     */
+    public void getCompInfo (Container pane)
+    {
+    }
+
+    /**
+     *  This method will get all components inside the JPopupMenu.
+     *  (Precondition: popup != null)
+     *  @param popup  its the JPopUpMenu we want to get the information from.
+     */
+    public void getPopUpMenuInfo (JPopupMenu popup)
+    {
+	Component[] group;
+	group = popup.getComponents();
+	SingleSelectionModel sm;
+	sm = popup.getSelectionModel();
+	int i;
+
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)popup.getComponent(i));
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get all the components inside the JRootPane.
+     *  (Precondition: pane != null)
+     *  @param pane  its the JRootPane we want to get the information from.
+     */
+    public void getRootPaneInfo (JRootPane pane)
+    {
+	Component[] group;
+	group = pane.getComponents();
+	int i;
+	
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			
+			getInside((Container)pane.getComponent(i));
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get all the components inside the JLayeredPane.
+     *  (Precondition: pane != null)
+     *  @param pane  its the JLayeredPane we want to get the information from.
+     */
+    public void getLayeredPaneInfo (JLayeredPane pane)
+    {
+	Component[] group;
+	group = pane.getComponents();
+	int i;
+	
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)pane.getComponent(i));
+		    }
+	    }
+    }
+
+    /**
+     * This is a dummy method to get BasicInternalFrameTitlePane.
+     */
+    public void getInternalFrameTitleInfo (BasicInternalFrameTitlePane pane)
+    {
+    }
+
+    /**
+     *  This method will add special column model and list selection
+     *  listener to JTableHeader.
+     *  (Precondition: header != null)
+     *  @param header  its the JTableHeader we want to get the information from.
+     */
+    public void getTableHeaderInfo (JTableHeader header)
+    {
+	TableColumnModel tcm;
+	tcm = header.getColumnModel();
+	ListSelectionModel lm;
+	lm = tcm.getSelectionModel();
+
+	if (tcm != null)
+	    {
+		if (hash.containsKey(tcm) == false)
+		    hash.put(tcm,header);
+		tcm.removeColumnModelListener(m);
+		tcm.addColumnModelListener(m);
+	    }
+
+	if (lm != null)
+	    {
+		if (hash.containsKey(lm) == false)
+		    hash.put(lm,header);
+		lm.removeListSelectionListener(m);
+		lm.addListSelectionListener(m);
+	    }
+    }
+
+    /**
+     *  This method will get all components inside the JToolBar.
+     *  (Precondition: tool != null)
+     *  @param tool  its the JToolBar we want to get the information from.
+     */
+    public void getToolBarInfo (JToolBar tool)
+    {
+	Component[] group;
+	group = tool.getComponents();
+	int i;
+	
+	if (group != null)
+	    {
+		for (i = 0; i < group.length ; i++)
+		    {
+			getInside((Container)tool.getComponent(i));
+		    }
+	    }
+    }
+
+    /**
+     *  This method will get all the components inside the JToolTip.
+     *  (Precondition: tip != null)
+     *  @param tip  its the JToolTip we want to get the information from.
+     */
+    public void getToolTipInfo (JToolTip tip)
+    {
+	Component group;
+	group = tip.getComponent();
+	int i;
+	
+	if (group != null)
+	    {
+		getInside((Container) group);
+	    }
+    }
+
+    /**
+     *  This is the dummy method to get JSeparator.
+     */
+    public void getSeparatorInfo (JSeparator sept)
+    {
+    }
+
+    /**
+     *  This method will get all the components inside the JDesktopPane.
+     *  (Precondition: pane != null)
+     *  @param pane  its the JDesktopPane we want to get the information from.
+     */
+    public void getDesktopPaneInfo (JDesktopPane pane)
+    {
+	JInternalFrame[]  group;
+	group = pane.getAllFrames();
+
+	if (group != null)
+	    {
+		int i;
+		
+		for ( i = 0 ; i < group.length ; i++ )
+		    {
+			getInside((Container) group[i]);
+		    }
+	    }
+    }
+
+    /**
+     *  This is a dummy method to catch null components.
+     */
+    public void getNullInfo ()
+    {
+    }
+
+    /**
+     *  This method will add a special listener to a container and components inside it.
+     *  This method is the gateway to all other methods in this class.
+     *  @param comp its the Container we want to get the information from.
+     */
+    public void getInside(Container comp)
+    {
+	if (comp == null)
+	    {
+		getNullInfo();
+		return;
+	    }
+	else if (comp instanceof JApplet)
+	    {
+		getAppletInfo ((JApplet) comp);
+		return;
+	    }
+	else if (comp instanceof JSeparator)
+	    {
+		getSeparatorInfo ((JSeparator) comp);
+		return;
+	    }
+	else if (comp instanceof JWindow)
+	    {
+		getWindowInfo((JWindow) comp);
+		return;
+	    }
+	else if (comp instanceof JDialog)
+	    {
+		getDialogInfo((JDialog) comp);
+		return;
+	    }
+	else if (comp instanceof JComboBox)
+	    {
+		getComboBoxInfo((JComboBox) comp);
+		return;
+	    }
+	else if (comp instanceof JLabel)
+	    {
+		getLabelInfo((JLabel) comp);
+		return;
+	    }
+	else if (comp instanceof JTextComponent)
+	    {
+		getTextAreaInfo((JTextComponent) comp);
+		return;
+	    }
+	else if (comp instanceof JFrame)
+	    {
+		getFrameInfo((JFrame) comp);
+		return;
+	    }
+	else if (comp instanceof JPanel)
+	    {
+		getPanelInfo((JPanel) comp);
+		return;
+	    }
+	else if (comp instanceof JScrollPane)
+	    {
+		getScrollInfo((JScrollPane) comp);
+		return;
+	    }
+	else if (comp instanceof JScrollBar)
+	    {
+		getScrollBarInfo ((JScrollBar) comp);
+		return;
+	    }
+	else if (comp instanceof JViewport)
+	    {
+		getViewPortInfo((JViewport) comp);
+		return;
+	    }
+	else if (comp instanceof JTabbedPane)
+	    {
+		getTabbedPaneInfo((JTabbedPane) comp);
+		return;
+	    }
+	else if (comp instanceof Box)
+	    {
+		getBoxInfo((Box) comp);
+		return;
+	    }
+	else if (comp instanceof JToggleButton)
+	    {
+		getToggleButtonInfo((JToggleButton) comp);
+		return;
+	    }
+	else if (comp instanceof JSplitPane)
+	    {
+		getSplitPaneInfo ((JSplitPane) comp);
+		return;
+	    }
+	else if (comp instanceof JList)
+	    {
+		getListInfo ((JList) comp);
+		return;
+	    }
+	else if (comp instanceof JTree)
+	    {
+		getTreeInfo ((JTree) comp);
+		return;
+	    }
+	else if (comp instanceof JTable)
+	    {
+		getTableInfo ((JTable) comp);
+		return;
+	    }
+	else if (comp instanceof JSlider)
+	    {
+		getSliderInfo ((JSlider) comp);
+		return;
+	    }
+	else if (comp instanceof JSpinner)
+	    {
+		getSpinnerInfo ((JSpinner) comp);
+		return;
+	    }
+	else if (comp instanceof JProgressBar)
+	    {
+		getProgressBarInfo ((JProgressBar) comp);
+		return;
+	    }
+	else if (comp instanceof JButton)
+	    {
+		getButtonInfo ((JButton) comp);
+		return;
+	    }
+	else if (comp instanceof JMenu)
+	    {
+		getMenuInfo ((JMenu) comp);
+		return;
+	    }
+	else if (comp instanceof JMenuBar)
+	    {
+		getMenuBarInfo ((JMenuBar) comp);
+		return;
+	    }
+	else if (comp instanceof JMenuItem)
+	    {
+		getMenuItemInfo ((JMenuItem) comp);
+		return;
+	    }
+	else if (comp instanceof JOptionPane)
+	    {
+		getOptionPaneInfo ((JOptionPane) comp);
+		return;
+	    }
+	else if (comp instanceof BasicInternalFrameTitlePane)
+	    {
+		getInternalFrameTitleInfo ((BasicInternalFrameTitlePane) comp);
+		return;
+	    }
+	else if (comp instanceof JColorChooser)
+	    {
+		getColorInfo ((JColorChooser) comp);
+		return;
+	    }
+	else if (comp instanceof JFileChooser)
+	    {
+		getFileChooserInfo ((JFileChooser) comp);
+		return;
+	    }
+	else if (comp instanceof JDesktopPane)
+	    {
+		getDesktopPaneInfo ((JDesktopPane) comp);
+		return;
+	    }
+	else if (comp instanceof JInternalFrame)
+	    {
+		getInternalFrameInfo ((JInternalFrame) comp);
+		return;
+	    }
+	else if (comp instanceof JLayeredPane)
+	    {
+		getLayeredPaneInfo ((JLayeredPane) comp);
+		return;
+	    }
+	else if (comp instanceof JPopupMenu)
+	    {
+		getPopUpMenuInfo ((JPopupMenu) comp);
+		return;
+	    }
+	else if (comp instanceof JRootPane)
+	    {
+		getRootPaneInfo ((JRootPane) comp);
+		return;
+	    }
+	else if (comp instanceof JTableHeader)
+	    {
+		getTableHeaderInfo ((JTableHeader) comp);
+		return;
+	    }
+	else if (comp instanceof JToolBar)
+	    {
+		getToolBarInfo ((JToolBar) comp);
+		return;
+	    }
+	else if (comp instanceof JToolTip)
+	    {
+		getToolTipInfo ((JToolTip) comp);
+		return;
+	    }
+	else
+	    {
+		getCompInfo (comp);
+		return;
+	    }
+    }
+}
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ComponentInformation.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ComponentInformation.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ComponentInformation.java	(revision 31635)
@@ -0,0 +1,2261 @@
+package org.greenstone.gatherer.feedback;
+
+import org.omg.IOP.ComponentIdHelper;
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.tree.*;
+import javax.swing.table.*;
+import javax.swing.text.*;
+import java.awt.image.*;
+import javax.imageio.ImageIO;
+import javax.swing.plaf.*;
+import javax.swing.plaf.basic.*;
+import javax.swing.colorchooser.*;
+import java.awt.event.*;
+import javax.swing.event.*;
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.text.MessageFormat;
+import javax.swing.plaf.metal.MetalIconFactory.*;
+import javax.swing.plaf.metal.MetalIconFactory;
+import javax.swing.plaf.basic.BasicInternalFrameTitlePane;
+import javax.swing.plaf.metal.MetalInternalFrameTitlePane;
+
+/**
+ *  In general, this class will get all information and add special listener 
+ *  to a Container and of all the components inside it.
+ *  <p>This class will get the information about the Container and all the components inside it.
+ *  The information we get is like whether or not the component is visible and the contents of it.
+ *  This class will also add special listener to the Container and each of the components inside 
+ *  the Container. So, it will allows special listener  to record and listen each of the actions 
+ *  the user do to the Container or any of the components inside it. The special listener
+ *  is an instance of ActionRecorderDialog.</p>
+ *  <p>This class will only recognized all the components that is belongs to the javax.swing package
+ *  which are all the implementing classes of RootPaneContainer interface and all the direct known
+ *  subclasses of JComponent. All the other components will be stated simply as Container 
+ *  and user cannot really get much information out of it.</p>
+ *  <p>The added special listener to this component is as what its allowed in API, if the component
+ *  have some modified listener added to it then that action will not be known and recorded by
+ *  the special listener.</p>
+ *  
+ *  @author Veronica Liesaputra
+ */
+public class ComponentInformation
+{
+    /**
+     *  This variable will hold the special listener that will listen and record any of the 
+     *  actions user do to the Container and any of the components inside it.
+     */
+    private ActionRecorderDialog m;
+    
+    /**
+     *  This variable will hold the resource of the words that is stored in Messages.properties file. 
+     *  The calling using messages.getString(someString) will caused someString to be translated
+     *  into some other string that is hold in that file.usually it will caused it to be translated
+     *  to the language that the user use or choose to have as stated in Locale.
+     */
+    private static ResourceBundle messages;
+
+    /**
+     *  This variable will hold the pair of the component and the model of the component.
+     *  With this variable allows ActionRecorderDialog listener to know which component the model that fires an action
+     *  belongs to. In this HashMap the model is the key and the component where the model belongs to is
+     *  the value. 
+     */
+    private HashMap hash;
+    
+    /**
+     *  This constructor will set the special listener, the locale and the hashmap to be used in
+     *  this Container.
+     *  (Precondition: dialog != null)
+     *  @param dialog  its the special listener that will listen and record all the action
+     *                 user do to any of the component inside the Container.
+     *  @param currentLocale its the locale that the user choose all the information to be displayed as.
+     *  @param h       its the HashMap that will hold the pair of model with the component it belongs.
+     */
+    public ComponentInformation 
+	(ActionRecorderDialog dialog,Locale currentLocale,HashMap h)
+    {
+	m = dialog;
+	hash = h;
+	messages = ResourceBundle.getBundle("feedback", currentLocale);
+    }
+	
+    /**
+     *  This method will get all information about the JTextComponent.
+     *  If the JTextComponent is a JTextField then it will add the special action listener to it.
+     *  (Precondition: txt != null)
+     *  @param txt its the JTextComponent we want to get all the information from.
+     *  @return    a UserComponent that hold the information about this
+     *             JTextComponent.
+     */
+    public  UserComponent getTextAreaInfo(JTextComponent txt)
+    {
+	UserComponent file;
+	file = new UserComponent(); 
+	String text;
+	text = txt.getText();
+
+	file.startContent(1);
+	if (txt instanceof JTextArea)
+	    file.saveContent("JTextArea");
+	else if (txt instanceof JTextPane)
+	    file.saveContent("JTextPane");
+	else if (txt instanceof JEditorPane)
+	    file.saveContent("JEditorPane");
+	else if (txt instanceof JTextField)
+	    {
+		((JTextField)txt).removeActionListener(m);
+		((JTextField)txt).addActionListener(m);
+		
+		if (txt instanceof JFormattedTextField)
+		    {
+			file.saveContent("JFormattedTextField");
+		    }
+		else if (txt instanceof JPasswordField)
+		    {
+			file.saveContent("JPasswordField");
+		    }
+		else
+		    {
+			file.saveContent("JTextField");
+		    }
+	    }
+
+	file.startContent(2);
+	file.saveContent(messages.getString("TextComponent"));
+	
+	file.startContent(3);
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+	
+	file.startContent(4);
+	file.saveContent(txt.getSelectedText());
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + txt.isVisible()));
+
+	file.startContent(7);
+	file.saveContent(txt.getToolTipText());
+	
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add the special action listener to the JButton.
+     *  (Precondition: button != null)
+     *  @param button its the JButton we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JButton.
+     */
+    public UserComponent getButtonInfo (JButton button)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	String text;
+	text = button.getText();
+
+	button.removeActionListener(m);
+	button.addActionListener(m);
+
+	file.startContent(1);
+	file.saveContent("JButton");
+
+	file.startContent(2);
+	file.saveContent(messages.getString("Button"));
+	
+	file.startContent(3);
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+
+	file.startContent(4);
+	file.saveContent(messages.getString("" + button.isSelected()));
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + button.isVisible()));
+	
+	file.startContent(6);
+	Icon img = button.getIcon();
+	if (img != null)
+	    {
+		file.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+	    }
+	else
+	    {
+		file.saveContent(messages.getString("NoImage"));
+	    }
+
+	file.startContent(7);
+	file.saveContent(button.getToolTipText());
+
+	return file;
+    }
+
+     /**
+     *  This method will get all the information and add the special change listener to the JProgressBar.
+     *  (Precondition: bar != null)
+     *  @param  bar   its the JProgressBar we want to get information from.
+     *  @return       a UserComponent that will hold the information about this JProgressBar.
+     */
+    public UserComponent getProgressBarInfo (JProgressBar bar)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	String text;
+	text = bar.getString();
+
+	bar.removeChangeListener(m);
+	bar.addChangeListener(m);
+	
+	file.startContent(1);
+	file.saveContent("JProgressBar");
+
+	file.startContent(2);
+	file.saveContent(messages.getString("ProgressBar"));
+	
+	file.startContent(3);
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + bar.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(bar.getToolTipText());
+
+	return file;
+    }
+    
+    /**
+     *  This method will get all the information and add the special change listener to the JSlider.
+     *  (Precondition: slider != null)
+     *  @param  slider its the JSlider we want to get information from.
+     *  @return        a UserComponent that will hold the information about this JSlider.
+     */
+    public UserComponent getSliderInfo (JSlider slider)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	String text;
+	text = "" + slider.getValue();
+	
+	slider.removeChangeListener(m);
+	slider.addChangeListener(m);
+
+	file.startContent(1);
+	file.saveContent("JSlider");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Slider"));
+	
+	file.startContent(3);
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + slider.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(slider.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add the special change listener to the JSpinner.
+     *  (Precondition: spinner != null)
+     *  @param spinner its the JSpinner we want to the information from.
+     *  @return        a UserComponent that will hold the information about this JSpinner.
+     */
+    public UserComponent getSpinnerInfo (JSpinner spinner)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	String text;
+	text = "" + spinner.getValue();
+	
+	spinner.removeChangeListener(m);
+	spinner.addChangeListener(m);
+
+	file.startContent(1);
+	file.saveContent("JSpinner");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Spinner"));
+	
+	file.startContent(3);
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + spinner.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(spinner.getToolTipText());
+
+	return file;
+    }
+    
+    /**
+     *  This method will get all the information of  the JLabel.
+     *  (Precondition: lbl != null)
+     *  @param lbl    its the JLabel we want to get information from.
+     *  @return       a UserComponent that will hold the information about this JLabel.
+     */
+    public UserComponent getLabelInfo(JLabel lbl)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	String text;
+	text = lbl.getText();
+
+	file.startContent(1);
+	file.saveContent("JLabel");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Label"));
+	
+	file.startContent(3);
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + lbl.isVisible()));
+
+	file.startContent(6);
+	Icon img = lbl.getIcon();
+	if (img != null)
+	    {
+		file.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+	    }
+	else
+	    {
+		file.saveContent(messages.getString("NoImage"));
+	    }
+	
+	file.startContent(7);
+	file.saveContent(lbl.getToolTipText());
+
+	return file;
+    }
+    
+    /**
+     *  This method will get all the information and add the special action listener to the JComboBox.
+     *  (Precondition: box != null)
+     *  @param box    its the JComboBox we want to get information from.
+     *  @return       a UserComponent that will hold the information about this JComboBox.
+     */
+    public  UserComponent getComboBoxInfo(JComboBox box)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	ComboBoxModel cm;
+	cm = box.getModel();
+
+	box.removeActionListener(m);
+	box.addActionListener(m);
+	
+	file.startContent(1);
+	file.saveContent("JComboBox");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("ComboBox"));
+	
+	file.startContent(3);
+	int i;
+	for (i = 0; i < box.getItemCount() ; i++)
+	    {
+		UserComponent file2;
+		file2 = new UserComponent();
+		//Assuming that they don't put any picture or images
+		String text;
+		Object objmsg;
+
+		objmsg = box.getItemAt(i);
+
+		if (objmsg instanceof String)
+		    text = (String) objmsg + "\n";
+		else
+		    text = objmsg.toString() + "\n" ;
+		
+		file2.startContent(3);
+		if (text != null)
+		    file2.saveContent(text);
+		else
+		    file2.saveContent(" ");
+		
+		file2.startContent(4);
+		if (i == box.getSelectedIndex())
+		    file2.saveContent(messages.getString("Selected"));
+		else
+		    file2.saveContent(messages.getString("NotSelected"));
+		
+		file.saveContent(file2);
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + box.isVisible()));
+
+	file.startContent(7);
+	file.saveContent(box.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add the special action listener to the JToggleButton.
+     *  (Precondition: button != null)
+     *  @param button its a JToggleButton inside the window
+     *  @return       a UserComponent that will hold the information about this JToggleButton.
+     */
+    public UserComponent getToggleButtonInfo (JToggleButton button)
+    {
+	UserComponent file;
+	file = new UserComponent();
+
+	button.removeActionListener(m);
+	button.addActionListener(m);
+
+	file.startContent(1);
+	if (button instanceof JCheckBox)
+	    file.saveContent("JCheckBox");
+	else
+	    file.saveContent("JRadioButton");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("ToggleButton"));
+	
+	file.startContent(3);
+	String text;
+	text = button.getText();
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+	
+	file.startContent(4);
+	if (button.isSelected() == true)
+	    {
+		file.saveContent(messages.getString("Selected"));
+	    }
+	else
+	    file.saveContent(messages.getString("NotSelected"));
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + button.isVisible()));
+	
+	file.startContent(6);
+	Icon img = button.getIcon();
+	if (img != null)
+	    {
+		file.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+	    }
+	else
+	    {
+		file.saveContent(messages.getString("NoImage"));
+	    }
+	
+	file.startContent(7);
+	file.saveContent(button.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add the special action listener to the JMenuItem.
+     *  (Precondition: menuitem != null)
+     *  @param menuitem its the JMenuItem we want to get the information from.
+     *  @return         a UserComponent that will hold the information about this JMenuItem.
+     */
+    public UserComponent getMenuItemInfo (JMenuItem menuitem)
+    {
+	UserComponent file;
+	file = new UserComponent();
+
+	menuitem.removeActionListener(m);
+	menuitem.addActionListener(m);
+	
+	file.startContent(1);
+	file.saveContent("JMenuItem");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("MenuItem"));
+	
+	file.startContent(3);
+	String text;
+	text = menuitem.getText();
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+	
+	file.startContent(4);
+	if (menuitem.isSelected() == true)
+	    {
+		file.saveContent(messages.getString("Selected"));
+	    }
+	else
+	    file.saveContent(messages.getString("NotSelected"));
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" +  menuitem.isVisible()));
+	
+	file.startContent(6);
+	Icon img = menuitem.getIcon();
+	if (img  != null)
+	    {
+		file.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+	    }
+	else
+	    {
+		file.saveContent(messages.getString("NoImage"));
+	    }
+	
+	file.startContent(7);
+	file.saveContent(menuitem.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JMenuBar.
+     *  (Precondition: menubar != null)
+     *  @param menubar its the JMenuBar we want to get the information from.
+     *  @return        a UserComponent that will hold the information about this JMenuBar.
+     */
+    public  UserComponent getMenuBarInfo (JMenuBar menubar)
+    {
+	UserComponent file;
+	file = new UserComponent();
+
+	file.startContent(1);
+	file.saveContent("JMenuBar");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("MenuBar"));
+	
+	file.startContent(3);
+	int i;
+	for (i= 0 ; i < menubar.getMenuCount() ; i++)
+	    {
+		file.saveContent(getInside(menubar.getMenu(i)));
+	    }
+	
+	file.startContent(4);
+	if (menubar.isSelected() == true)
+	    {
+		file.saveContent(messages.getString("Selected"));
+	    }
+	else
+	    file.saveContent(messages.getString("NotSelected"));
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" +  menubar.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(menubar.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add the special action and menu listener to the JMenu.
+     *  (Precondition: menu != null)
+     *  @param menu   its the JMenu we want to the the information from.
+     *  @return       a UserComponent that will hold the information about this JMenu.
+     */
+    public  UserComponent getMenuInfo (JMenu menu)
+    {
+	UserComponent file;
+	file = new UserComponent();
+
+	menu.removeActionListener(m);
+	menu.removeMenuListener(m);
+	menu.addActionListener(m);
+	menu.addMenuListener(m);
+
+	file.startContent(1);
+	file.saveContent("JMenu");
+	
+	file.startContent(2);
+	String text;
+	text = menu.getText();
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+	
+	file.startContent(3);
+	int i;
+	for (i= 0 ; i < menu.getMenuComponentCount() ; i++)
+	    {
+		file.saveContent(getInside((Container)menu.getMenuComponent(i)));
+	    }
+	
+	file.startContent(4);
+	if (menu.isSelected() == true)
+	    {
+		file.saveContent(messages.getString("Selected"));
+	    }
+	else
+	    file.saveContent(messages.getString("NotSelected"));
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" +  menu.isVisible()));
+	
+	file.startContent(6);
+	Icon img = menu.getIcon();
+	if (img  != null)
+	    {
+		file.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+	    }
+	else
+	    {
+		file.saveContent(messages.getString("NoImage"));
+	    }
+	
+	file.startContent(7);
+	file.saveContent(menu.getToolTipText());
+
+	return file;
+    }
+    
+    /**
+     *  This method will get all the information of the JApplet.
+     *  (Precondition: applet != null)
+     *  @param applet its the JApplet we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JApplet.
+     */
+    public  UserComponent getAppletInfo (JApplet applet)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = applet.getComponents();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("JApplet");
+	
+	file.startContent(2);
+	String text;
+	text = applet.getAppletInfo();
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)applet.getComponent(i)));
+		    }
+	    }
+	
+	file.startContent(4);
+	file.saveContent(messages.getString("" + applet.isActive()));
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + applet.isVisible()));
+	
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JWindow.
+     *  (Precondition: window != null)
+     *  @param window its the JWindow we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JWindow.
+     */
+    public  UserComponent getWindowInfo (JWindow window)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = window.getComponents();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("JWindow");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Window"));
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)window.getComponent(i)));
+		    }
+	    }
+	
+	file.startContent(4);
+	file.saveContent(messages.getString("" + window.isActive()));
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + window.isVisible()));
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JDialog.
+     *  (Precondition: dialog != null)
+     *  @param dialog its the JDialog we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JDialog.
+     */
+    public UserComponent getDialogInfo (JDialog dialog)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = dialog.getComponents();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("JDialog");
+	
+	file.startContent(2);
+	String text = dialog.getTitle();
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)dialog.getComponent(i)));
+		    }
+	    }
+	
+	file.startContent(4);
+	file.saveContent(messages.getString("" + dialog.isActive()));
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + dialog.isVisible()));
+	
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JInternalFrame.
+     *  (Precondition: frame != null)
+     *  @param frame  its the JInternalFrame we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JInternalFrame.
+     */
+    public UserComponent getInternalFrameInfo (JInternalFrame frame)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = frame.getComponents();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("JInternalFrame");
+	
+	file.startContent(2);
+	String text;
+	text = frame.getTitle();
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)frame.getComponent(i)));
+		    }
+	    }
+	
+	file.startContent(4);
+	file.saveContent(messages.getString("" + frame.isSelected()));
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + frame.isVisible()));
+	
+	file.startContent(6);
+	Icon img = frame.getFrameIcon();
+	if (img != null)
+	    {
+		file.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+	    }
+	else
+	    {
+		file.saveContent(messages.getString("NoImage"));
+	    }
+	
+	file.startContent(7);
+	file.saveContent(frame.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JFrame.
+     *  (Precondition: frame != null)
+     *  @param frame  its the JFrame we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JFrame.
+     */
+    public UserComponent getFrameInfo (JFrame frame)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = frame.getComponents();
+	int i;
+	
+	file.startContent(1);
+	file.saveContent("JFrame");
+	
+	file.startContent(2);
+	String text;
+	text = frame.getTitle();
+	if (text != null)
+	    file.saveContent(text);
+	else
+	    file.saveContent(" ");
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)frame.getComponent(i)));
+		    }
+	    }
+	
+	file.startContent(4);
+	file.saveContent(messages.getString("" + frame.isActive()));
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + frame.isVisible()));
+	
+	file.startContent(6);
+	Image image;
+	image = frame.getIconImage();
+	if (image != null)
+	    {
+		ImageIcon img;
+		img = new ImageIcon(image);
+		file.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+	    }
+	else
+	    {
+		file.saveContent(messages.getString("NoImage"));
+	    }
+	
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JPanel.
+     *  (Precondition: pane != null)
+     *  @param pane   its the JPanel we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JPanel.
+     */
+    public UserComponent getPanelInfo (JPanel pane)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = pane.getComponents();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("JPanel");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Panel"));
+
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)pane.getComponent(i)));
+		    }
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + pane.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(pane.getToolTipText());
+
+	return file;
+    }
+    
+    /**
+     *  This method will get all the information of the JScrollPane.
+     *  (Precondition: scroll != null)
+     *  @param scroll its the JScrollPane we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JScrollPane.
+     */
+    public UserComponent getScrollInfo (JScrollPane scroll)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = scroll.getComponents();
+	int i;
+	
+	file.startContent(1);
+	file.saveContent("JScrollPane");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Scroller"));
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)group[i]));
+		    }
+	    }
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + scroll.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(scroll.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add special change listener to the JTabbedPane.
+     *  (Precondition: tab != null)
+     *  @param tab   its the JTabbedPane we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this JTabbedPane.
+     */
+    public UserComponent getTabbedPaneInfo (JTabbedPane tab)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	int j;
+	
+	tab.removeChangeListener(m);
+	tab.addChangeListener(m);
+
+	file.startContent(1);
+	file.saveContent("JTabbedPane");
+	
+	file.startContent(3);
+	for (j = 0 ; j < tab.getTabCount(); j++)
+	    {	
+		UserComponent file2;
+		file2 = new UserComponent();
+
+		file2.startContent(2);
+		String title;
+		title = tab.getTitleAt(j);
+		if (title != null)
+		    file2.saveContent(title);
+		else
+		    file2.saveContent(" ");
+		
+		file2.startContent(6);
+		Icon img;
+		img = tab.getIconAt(j);
+		if (img != null)
+		    {
+			file2.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+		    }
+		else
+		    {
+			file2.saveContent(messages.getString("NoImage"));
+		    }
+		
+		file2.startContent(3);
+		Component group = tab.getComponentAt(j);
+		file2.saveContent(getInside((Container)group));
+		
+		file2.startContent(4);
+		if (j == tab.getSelectedIndex())
+		    file2.saveContent(messages.getString("Selected"));
+		else
+		    file2.saveContent(messages.getString("NotSelected"));
+		
+		file.saveContent(file2);
+	    }
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + tab.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(tab.getToolTipText());
+
+	return file;
+    }
+    
+    /**
+     *  This method will get all the information of the JOptionPane.
+     *  (Precondition: op != null)
+     *  @param op   its the JOptionPane we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this JOptionPane.
+     */
+    public UserComponent getOptionPaneInfo (JOptionPane op)
+    {
+	UserComponent file;
+	file = new UserComponent();
+
+	file.startContent(1);
+	file.saveContent("JOptionPane");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("OptionPane"));
+	
+	file.startContent(3);
+	Object objmsg;
+	objmsg = op.getMessage();
+
+	if (objmsg != null)
+	    {
+		String text;
+		if (objmsg instanceof String)
+		    text = (String) objmsg + "\n";
+		else
+		    text = objmsg.toString() + "\n" ;
+		file.saveContent(text);
+	    }
+	else
+	    file.saveContent(" ");
+	
+	Object[] obj;
+	int i;
+	obj = op.getOptions();
+	if (obj != null)
+	    {
+		for (i = 0; i < obj.length ; i++)
+		    {
+			if (obj[i] instanceof String)
+			    file.saveContent((String) obj[i]);
+			else
+			    file.saveContent(obj[i].toString());
+		    }
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + op.isVisible()));
+	
+	file.startContent(6);
+	Icon img = op.getIcon();
+	if (img != null)
+	    {
+		file.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+	    }
+	else
+	    {
+		file.saveContent(messages.getString("NoImage"));
+	    }
+	
+	file.startContent(7);
+	file.saveContent(op.getToolTipText());
+
+	return file;
+	
+    }
+
+    /**
+     *  This method will get all the information of the JViewPort.
+     *  (Precondition: vp != null)
+     *  @param vp   its the JViewPort we want to get the information from.
+     *  @return     a UserComponent that will hold the information about this JViewPort.
+     */
+    public  UserComponent getViewPortInfo (JViewport vp)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = vp.getComponents();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("JViewPort");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("ViewPort"));
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)group[i]));
+		    }
+	    }
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + vp.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(vp.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the Box.
+     *  (Precondition: box != null)
+     *  @param box  its the Box we want to get the information from.
+     *  @return     a UserComponent that will hold the information about this Box.
+     */
+    public UserComponent getBoxInfo (Box box)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = box.getComponents();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("Box");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Box"));
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)group[i]));
+		    }
+	    }
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + box.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(box.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JSplitPane.
+     *  (Precondition: split != null)
+     *  @param split  its the JSplitPane we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JSplitPane.
+     */
+    public  UserComponent getSplitPaneInfo (JSplitPane split)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = split.getComponents();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("JSplitPane");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Splitters"));
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)group[i]));
+		    }
+	    }
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + split.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(split.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add special list selection listener to the JList.
+     *  (Precondition: list != null)
+     *  @param list  its the JList we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this JList.
+     */
+    public UserComponent getListInfo (JList list)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	ListModel ls;
+	ls = list.getModel();
+	int i;
+	
+	list.removeListSelectionListener(m);
+	list.addListSelectionListener(m);
+	
+	if (ls != null)
+	    {
+		if (hash.containsKey(ls) == false)
+		    hash.put(ls,list);
+		ls.removeListDataListener(m);
+		ls.addListDataListener(m);
+	    }
+
+	file.startContent(1);
+	file.saveContent("JList");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("List"));
+	
+	if (ls != null)
+	    {
+		file.startContent(3);
+		for ( i = 0 ; i < ls.getSize() ; i++)
+		    {
+			UserComponent file2;
+			file2 = new UserComponent();
+			//Assume that its all a text
+			file2.startContent(3);
+			String text;
+			Object obj;
+			obj = ls.getElementAt(i);
+			
+			if (obj instanceof String)
+			    text = (String) obj;
+			else
+			    text = obj.toString();
+			
+			if (text != null)
+			    file2.saveContent(text);
+			else
+			    file2.saveContent(" ");
+			
+			file2.startContent(4);
+			if ( i == list.getSelectedIndex())
+			    {
+				file2.saveContent(messages.getString("Selected"));
+			    }
+			else
+			    file2.saveContent(messages.getString("NotSelected"));
+			
+			file.saveContent(file2);
+		    }
+	    }
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + list.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(list.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add special change listener to the JColorChooser.
+     *  (Precondition: color != null)
+     *  @param color  its the JColorChooser we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JColorChooser.
+     */
+    public UserComponent getColorInfo (JColorChooser color)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	ColorSelectionModel cm;
+	cm = color.getSelectionModel();
+	int i;
+	
+	if (cm != null)
+	    {
+		if (hash.containsKey(cm) == false)
+		    hash.put(cm,color);
+		cm.removeChangeListener(m);
+		cm.addChangeListener(m);
+	    }
+	
+	file.startContent(1);
+	file.saveContent("JColorChooser");
+
+	file.startContent(2);
+	file.saveContent(messages.getString("ColorChooser"));
+
+	file.startContent(3);
+	UserComponent file2;
+	file2 = new UserComponent();
+	file2.startContent(2);
+	file2.saveContent(messages.getString("PreviewPanel"));
+
+	file2.startContent(3);
+	file2.saveContent(getInside((Container) color.getPreviewPanel()));
+	
+	file.saveContent(file2);
+
+	file2 = new UserComponent();
+	file2.startContent(2);
+	file2.saveContent(messages.getString("ChooserPanels"));
+
+	AbstractColorChooserPanel[] cp;
+	cp = color.getChooserPanels();
+	if ((cp != null) && (cp.length != 0))
+	    {
+		file2.startContent(3);
+		for (i = 0 ; i < cp.length ; i++)
+		    {
+			UserComponent file3;
+			file3 = new UserComponent();
+			file3.startContent(2);
+			file3.saveContent(cp[i].getDisplayName());
+			
+			ColorSelectionModel cpsm;
+			cpsm = cp[i].getColorSelectionModel();
+
+			if (cpsm != null)
+			    {
+				file3.startContent(3);
+				file3.saveContent("" + 
+						  cpsm.getSelectedColor());
+			    }
+
+			file3.startContent(6);
+			Icon img = cp[i].getSmallDisplayIcon();
+			if (img  != null)
+			    {
+				file3.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+			    }
+			else
+			    {
+				file3.saveContent(messages.getString("NoImage"));
+			    }
+			
+			file3.startContent(6);
+			img = cp[i].getLargeDisplayIcon();
+			if (img  != null)
+			    {
+				file3.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+			    }
+			else
+			    {
+				file3.saveContent(messages.getString("NoImage"));
+			    }
+			file2.saveContent(file3);
+		    }
+	    }
+		
+	file.saveContent(file2);    
+	
+	file.startContent(4);
+	file.saveContent("" + cm.getSelectedColor().toString());
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" +  color.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(color.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add special action listener to the JFileChooser.
+     *  (Precondition: fc != null)
+     *  @param fc  its the JFileChooser we want to get the information from.
+     *  @return    a UserComponent that will hold the information about this JFileChooser.
+     */
+    public UserComponent getFileChooserInfo (JFileChooser fc)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	int i;
+
+	fc.removeActionListener(m);
+	fc.addActionListener(m);
+
+	file.startContent(1);
+	file.saveContent("JFileChooser");
+	
+	file.startContent(2);
+	file.saveContent(fc.getDialogTitle());
+
+	File[] f;
+	f = fc.getSelectedFiles();
+	
+	if (f != null)
+	    {
+		file.startContent(3);
+		
+		for ( i = 0 ; i < f.length ; i++)
+		    {
+			UserComponent file2;
+			file2 = new UserComponent();
+			file2.startContent(1);
+			file2.saveContent(fc.getTypeDescription(f[i]));
+			
+			file2.startContent(2);
+			file2.saveContent(fc.getName(f[i]));
+			
+			file2.startContent(6);
+			Icon img = fc.getIcon(f[i]);
+			if (img  != null)
+			    {
+				file2.saveImage(img,messages.getString("icon")+img.hashCode()+".jpg");
+			    }
+			else
+			    {
+				file2.saveContent(messages.getString("NoImage"));
+			    }
+			file.saveContent(file2);
+		    }
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" +  fc.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(fc.getToolTipText());
+
+	return file;
+    }		
+
+    /**
+     *  This method will get all the information and add special list selection and column model listener 
+     *  to the JTable.
+     *  (Precondition: table != null)
+     *  @param table  its the JTable we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JTable.
+     */
+    public UserComponent getTableInfo (JTable table)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	TableModel tm;
+	tm = table.getModel();
+	int i,j;
+	String text;
+
+	TableColumnModel tcm;
+	tcm = table.getColumnModel();
+	ListSelectionModel lm;
+	lm = table.getSelectionModel(); 
+
+	if (lm != null)
+	    {
+		if (hash.containsKey(lm) == false)
+		    hash.put(lm,table);
+		lm.removeListSelectionListener(m);
+		lm.addListSelectionListener(m);
+	    }
+	
+	if (tcm != null)
+	    {
+		if (hash.containsKey(tcm) == false)
+		    hash.put(tcm,table);
+		tcm.removeColumnModelListener(m);
+		tcm.addColumnModelListener(m);
+	    }
+
+	file.startContent(1);
+	file.saveContent("JTable");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Table"));
+	
+	if (tm != null)
+	    {
+		file.startContent(3);
+		for ( i = 0 ; i < tm.getRowCount() ; i++)
+		    {
+			UserComponent file2;
+			file2 = new UserComponent();
+			
+			file2.startContent(2);
+			text = messages.getString("row") +" " + (i + 1);
+			file2.saveContent(text);
+			
+			file2.startContent(3);
+			for ( j = 0 ; j < tm.getColumnCount() ; j++)
+			    {
+				UserComponent file3;
+				file3 = new UserComponent();
+				
+				file3.startContent(2);
+				file3.saveContent(tm.getColumnName(j));
+				
+				file3.startContent(3);
+				
+				Object obj;
+				obj = tm.getValueAt(i,j);
+				
+				if (obj instanceof String)
+				    file3.saveContent((String)obj);
+				else
+				    file3.saveContent(obj.toString());
+				
+				file3.startContent(4);
+				if (table.isCellSelected(i,j) == true)
+				    {
+					file3.saveContent(messages.getString("Selected"));
+				    }
+				else
+				    file3.saveContent(messages.getString("NotSelected"));
+				
+				file2.saveContent(file3);
+			    }
+			
+			file.saveContent(file2);
+		    }
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" +  table.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(table.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JScrollBar.
+     *  (Precondition: scroll != null)
+     *  @param scroll  its the JScrollBar we want to get the information from.
+     *  @return         a UserComponent that will hold the information about this JScrollBar.
+     */
+    public  UserComponent getScrollBarInfo (JScrollBar scroll)
+    {
+	UserComponent file;
+	file = new UserComponent();
+
+	file.startContent(1);
+	file.saveContent("JScrollBar");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("ScrollerBar"));
+	
+	file.startContent(3);
+	file.saveContent(""+ scroll.getOrientation()+ " :"+ scroll.getValue());
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + scroll.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(scroll.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add special tree selection listener to the JTree.
+     *  (Precondition: tree != null)
+     *  @param tree  its the JTree we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this JTree.
+     */
+    public UserComponent getTreeInfo (JTree tree)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	TreeModel tm;
+	tm = tree.getModel();
+	int i;
+	
+	tree.removeTreeSelectionListener(m);
+	tree.addTreeSelectionListener(m);
+	
+	if (tm != null)
+	    {
+		if (hash.containsKey(tm) == false)
+		    hash.put(tm,tree);
+		tm.removeTreeModelListener(m);
+		tm.addTreeModelListener(m);
+	    }
+	
+	file.startContent(1);
+	file.saveContent("JTree");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Tree"));
+	
+	file.startContent(3);
+	Object root;
+	root = tm.getRoot();
+	file.saveContent(getChildInfo(tm,root,tree));
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" +  tree.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(tree.getToolTipText());
+
+	return file;
+    }
+       
+    /**
+     *  This method will get all the information about the parent node and all the childs of it inside the JTree.
+     *  (Precondition: (tree != null) && (tm != null) && (parent != null))
+     *  @param tm      its the tree model of the JTree.
+     *  @param parent  its the parent node that we want to get all the child information from.
+     *  @param tree    its the JTree where the parent node belongs to.
+     *  @return        a UserComponent that will hold the information about this parent node.  
+     */
+    private UserComponent getChildInfo (TreeModel tm,Object parent,JTree tree)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	if (tm.isLeaf(parent) == false)
+	    {
+		file.startContent(2);
+		String text;
+		text = parent.toString();
+		if (text != null)
+		    file.saveContent(text);
+		else
+		    file.saveContent(" ");
+		
+		int j;
+		for (j = 0 ; j < tm.getChildCount(parent) ; j++)
+		    {
+			file.startContent(3);
+			Object child;
+			child = tm.getChild(parent,j);
+			file.saveContent(getChildInfo(tm,child,tree));
+		    }
+	    }
+	else
+	    {
+		file.startContent(2);
+		String text;
+		text = parent.toString();
+		if (text != null)
+		    file.saveContent(text);
+		else
+		    file.saveContent(" ");
+	    }
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of a Container that is not belongs to any of the more specified
+     *  method stated in this class.
+     *  (Precondition: pane != null)
+     *  @param pane  its the Container component we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this Container.
+     */
+    public UserComponent getCompInfo (Container pane)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = pane.getComponents();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("Container");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Container"));
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		file.saveContent(messages.getString("TitlePane"));
+		file.saveContent(getInside(null));
+		/*for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)pane.getComponent(i)));
+			}*/
+	    }
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + pane.isVisible()));
+	
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JPopupMenu.
+     *  (Precondition: popup != null)
+     *  @param popup  its the JPopUpMenu we want to get the information from.
+     *  @return       a UserComponent that will hold the information about this JPopupMenu.
+     */
+    public UserComponent getPopUpMenuInfo (JPopupMenu popup)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = popup.getComponents();
+	SingleSelectionModel sm;
+	sm = popup.getSelectionModel();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("JPopUpMenu");
+
+	file.startContent(2);
+	String text;
+	text = popup.getLabel();
+	if (text != null)
+	    file.saveContent(popup.getLabel());
+	else
+	    file.saveContent(" ");
+
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)popup.getComponent(i)));
+		    }
+	    }
+
+	if (sm != null)
+	    {
+		file.startContent(4);
+		file.saveContent("" + sm.getSelectedIndex());
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + popup.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(popup.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JRootPane.
+     *  (Precondition: pane != null)
+     *  @param pane  its the JRootPane we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this JRootPane.
+     */
+    public UserComponent getRootPaneInfo (JRootPane pane)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = pane.getComponents();
+	int i;
+	
+	file.startContent(1);
+	file.saveContent("JRootPane");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("RootPane"));
+
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			
+			file.saveContent(getInside((Container)pane.getComponent(i)));
+		    }
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + pane.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(pane.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JLayeredPane.
+     *  (Precondition: pane != null)
+     *  @param pane  its the JLayeredPane we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this JLayeredPane.
+     */
+    public UserComponent getLayeredPaneInfo (JLayeredPane pane)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = pane.getComponents();
+	int i;
+	
+	file.startContent(1);
+	file.saveContent("JLayeredPane");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("LayeredPane"));
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)pane.getComponent(i)));
+		    }
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + pane.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(pane.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the BasicInternalFrameTitlePane.
+     *  (Precondition: pane != null)
+     *  @param pane  its the BasicInternalFrameTitlePane we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this BasicInternalFrameTitlePane.
+     */
+    public UserComponent getInternalFrameTitleInfo (BasicInternalFrameTitlePane pane)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = pane.getComponents();
+	int i;
+
+	file.startContent(1);
+	file.saveContent("BasicInternalFrameTitlePane");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("InternalFrameTitlePane"));
+	
+	/*if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			Component c;
+			c = pane.getComponent(i);
+			
+			else
+			    file.saveContent(getInside((Container)pane.getComponent(i)));
+		    }
+		    }*/
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + pane.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(pane.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information and add special column model and list selection
+     *  listener to JTableHeader.
+     *  (Precondition: header != null)
+     *  @param header  its the JTableHeader we want to get the information from.
+     *  @return        a UserComponent that will hold the information about this JTableHeader.
+     */
+    public UserComponent getTableHeaderInfo (JTableHeader header)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	TableColumnModel tcm;
+	tcm = header.getColumnModel();
+	ListSelectionModel lm;
+	lm = tcm.getSelectionModel();
+
+	if (tcm != null)
+	    {
+		if (hash.containsKey(tcm) == false)
+		    hash.put(tcm,header);
+		tcm.removeColumnModelListener(m);
+		tcm.addColumnModelListener(m);
+	    }
+
+	if (lm != null)
+	    {
+		if (hash.containsKey(lm) == false)
+		    hash.put(lm,header);
+		lm.removeListSelectionListener(m);
+		lm.addListSelectionListener(m);
+	    }
+
+	file.startContent(1);
+	file.saveContent("JTableHeader");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("TableHeader"));
+
+	if (tcm != null)
+	    {
+		int[] select = tcm.getSelectedColumns();
+
+		if (select != null)
+		    {
+			file.startContent(3);
+			for (int i = 0 ; i < select.length ; i++)
+			    {
+				UserComponent file2;
+				file2 = new UserComponent();
+				file2.startContent(4);
+				file2.saveContent("" + select[i]);
+				
+				file.saveContent(file2);
+			    }
+		    }
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + header.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(header.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JToolBar.
+     *  (Precondition: tool != null)
+     *  @param tool  its the JToolBar we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this JToolBar.
+     */
+    public UserComponent getToolBarInfo (JToolBar tool)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component[] group;
+	group = tool.getComponents();
+	int i;
+	
+	file.startContent(1);
+	file.saveContent("JToolBar");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("ToolBar"));
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		for (i = 0; i < group.length ; i++)
+		    {
+			file.saveContent(getInside((Container)tool.getComponent(i)));
+		    }
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + tool.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(tool.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JToolTip.
+     *  (Precondition: tip != null)
+     *  @param tip  its the JToolTip we want to get the information from.
+     *  @return     a UserComponent that will hold the information about this JToolTip.
+     */
+    public UserComponent getToolTipInfo (JToolTip tip)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	Component group;
+	group = tip.getComponent();
+	int i;
+	
+	file.startContent(1);
+	file.saveContent("JToolTip");
+	
+	file.startContent(2);
+	String text;
+	text = tip.getTipText();
+	if (text != null)
+	    file.saveContent(tip.getTipText());
+	else
+	    file.saveContent(" ");
+	
+	if (group != null)
+	    {
+		file.startContent(3);
+		file.saveContent(getInside((Container) group));
+	    }
+	
+	file.startContent(5);
+	file.saveContent(messages.getString("" + tip.isVisible()));
+	
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JSeparator.
+     *  (Precondition: sept != null)
+     *  @param sept  its the JSeparator we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this JSeparator.
+     */
+    public UserComponent getSeparatorInfo (JSeparator sept)
+    {
+	UserComponent file;
+	file = new UserComponent();
+
+	file.startContent(1);
+	file.saveContent("JSeparator");
+	
+	file.startContent(2);
+	file.saveContent(messages.getString("Separator"));
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + sept.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(sept.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will get all the information of the JDesktopPane.
+     *  (Precondition: pane != null)
+     *  @param pane  its the JDesktopPane we want to get the information from.
+     *  @return      a UserComponent that will hold the information about this JDesktopPane.
+     */
+    public UserComponent getDesktopPaneInfo (JDesktopPane pane)
+    {
+	UserComponent file;
+	file = new UserComponent();
+	JInternalFrame[]  group;
+	group = pane.getAllFrames();
+
+	file.startContent(1);
+	file.saveContent("JDesktopPane");
+
+	file.startContent(2);
+	file.saveContent(messages.getString("DesktopPane"));
+
+	if (group != null)
+	    {
+		int i;
+		file.startContent(3);
+		for ( i = 0 ; i < group.length ; i++ )
+		    {
+			file.saveContent (getInside((Container) group[i]));
+		    }
+	    }
+
+	file.startContent(5);
+	file.saveContent(messages.getString("" + pane.isVisible()));
+	
+	file.startContent(7);
+	file.saveContent(pane.getToolTipText());
+
+	return file;
+    }
+
+    /**
+     *  This method will give you a UserComponent that hold the information saying
+     *  the component is null (no component).
+     *  @return  a UserComponent that will hold the information saying its a null component.
+     */
+    public UserComponent getNullInfo ()
+    {
+	UserComponent file;
+	file = new UserComponent();
+
+	file.startContent(1);
+	file.saveContent(messages.getString("NoComponent"));
+
+	return file;
+    }
+
+    /**
+     *  This method will get all information about any kind of Container.
+     *  This method is the gateway to all other methods in this class and will give you all
+     *  information about the Container and whats inside it.If you dont know what the specific class
+     *  of your container then you go here and it will sort which appropriate method 
+     *  this container should go to in this class.
+     *  @param comp its the Container we want to get the information from.
+     *  @return     a UserComponent that hold the information about this Container.
+     */
+    public  UserComponent getInside(Container comp)
+    {
+	if (comp == null)
+	    {
+		return getNullInfo();
+	    }
+	else if (comp instanceof JApplet)
+	    {
+		return getAppletInfo ((JApplet) comp);
+	    }
+	else if (comp instanceof JSeparator)
+	    {
+		return getSeparatorInfo ((JSeparator) comp);
+	    }
+	else if (comp instanceof JWindow)
+	    {
+		return getWindowInfo((JWindow) comp);
+	    }
+	else if (comp instanceof JDialog)
+	    {
+		return getDialogInfo((JDialog) comp);
+	    }
+	else if (comp instanceof JComboBox)
+	    {
+		return getComboBoxInfo((JComboBox) comp);
+	    }
+	else if (comp instanceof JLabel)
+	    {
+		return getLabelInfo((JLabel) comp);
+	    }
+	else if (comp instanceof JTextComponent)
+	    {
+		return getTextAreaInfo((JTextComponent) comp);
+	    }
+	else if (comp instanceof JFrame)
+	    {
+		return getFrameInfo((JFrame) comp);
+	    }
+	else if (comp instanceof JPanel)
+	    {
+		return getPanelInfo((JPanel) comp);
+	    }
+	else if (comp instanceof JScrollPane)
+	    {
+		return getScrollInfo((JScrollPane) comp);
+	    }
+	else if (comp instanceof JScrollBar)
+	    {
+		return getScrollBarInfo ((JScrollBar) comp);
+	    }
+	else if (comp instanceof JViewport)
+	    {
+		return getViewPortInfo((JViewport) comp);
+	    }
+	else if (comp instanceof JTabbedPane)
+	    {
+		return getTabbedPaneInfo((JTabbedPane) comp);
+	    }
+	else if (comp instanceof Box)
+	    {
+		return getBoxInfo((Box) comp);
+	    }
+	else if (comp instanceof JToggleButton)
+	    {
+		return getToggleButtonInfo((JToggleButton) comp);
+	    }
+	else if (comp instanceof JSplitPane)
+	    {
+		return getSplitPaneInfo ((JSplitPane) comp);
+	    }
+	else if (comp instanceof JList)
+	    {
+		return getListInfo ((JList) comp);
+	    }
+	else if (comp instanceof JTree)
+	    {
+		return getTreeInfo ((JTree) comp);
+	    }
+	else if (comp instanceof JTable)
+	    {
+		return getTableInfo ((JTable) comp);
+	    }
+	else if (comp instanceof JSlider)
+	    {
+		return getSliderInfo ((JSlider) comp);
+	    }
+	else if (comp instanceof JSpinner)
+	    {
+		return getSpinnerInfo ((JSpinner) comp);
+	    }
+	else if (comp instanceof JProgressBar)
+	    {
+		return getProgressBarInfo ((JProgressBar) comp);
+	    }
+	else if (comp instanceof JButton)
+	    {
+		return getButtonInfo ((JButton) comp);
+	    }
+	else if (comp instanceof JMenu)
+	    {
+		return getMenuInfo ((JMenu) comp);
+	    }
+	else if (comp instanceof JMenuBar)
+	    {
+		return getMenuBarInfo ((JMenuBar) comp);
+	    }
+	else if (comp instanceof JMenuItem)
+	    {
+		return getMenuItemInfo ((JMenuItem) comp);
+	    }
+	else if (comp instanceof JOptionPane)
+	    {
+		return getOptionPaneInfo ((JOptionPane) comp);
+	    }
+	else if (comp instanceof BasicInternalFrameTitlePane)
+	    {
+		return getInternalFrameTitleInfo ((BasicInternalFrameTitlePane) comp);
+	    }
+	else if (comp instanceof JColorChooser)
+	    {
+		return getColorInfo ((JColorChooser) comp);
+	    }
+	else if (comp instanceof JFileChooser)
+	    {
+		return getFileChooserInfo ((JFileChooser) comp);
+	    }
+	else if (comp instanceof JDesktopPane)
+	    {
+		return getDesktopPaneInfo ((JDesktopPane) comp);
+	    }
+	else if (comp instanceof JInternalFrame)
+	    {
+		return getInternalFrameInfo ((JInternalFrame) comp);
+	    }
+	else if (comp instanceof JLayeredPane)
+	    {
+		return getLayeredPaneInfo ((JLayeredPane) comp);
+	    }
+	else if (comp instanceof JPopupMenu)
+	    {
+		return getPopUpMenuInfo ((JPopupMenu) comp);
+	    }
+	else if (comp instanceof JRootPane)
+	    {
+		return getRootPaneInfo ((JRootPane) comp);
+	    }
+	else if (comp instanceof JTableHeader)
+	    {
+		return getTableHeaderInfo ((JTableHeader) comp);
+	    }
+	else if (comp instanceof JToolBar)
+	    {
+		return getToolBarInfo ((JToolBar) comp);
+	    }
+	else if (comp instanceof JToolTip)
+	    {
+		return getToolTipInfo ((JToolTip) comp);
+	    }
+	else
+	    {
+		return getCompInfo (comp);
+	    }
+    }
+}
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/FeedbackInterface.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/FeedbackInterface.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/FeedbackInterface.java	(revision 31635)
@@ -0,0 +1,1645 @@
+package org.greenstone.gatherer.feedback;
+
+import java.io.*;
+import java.awt.*;
+import java.awt.image.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import java.util.*;
+import java.net.*;
+import java.rmi.server.*;
+import javax.imageio.*;
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.text.MessageFormat;
+import javax.swing.Timer;
+import org.greenstone.gatherer.Gatherer;
+
+/**
+ * This class will show the FeedbackInterface form.
+ * It will allow user to described about the problem better.Here we will also take some more information available
+ * in the system such as the size of the memory available and the screen size.User also could take more screenshot
+ * so they will be able to say the sequence of events that lead up to the problem.
+ * <br>If the user don't want to send this form, then all the information taken from this form will be deleted 
+ * and user will be quitted from the Reporting Feedback sequence.
+ *
+ * @author Veronica Liesaputra
+ */
+public class FeedbackInterface extends JDialog
+    implements ActionListener
+{
+    /**
+     * This is the button user need to pressed if they want to view the files.
+     * This button will be disabled when the timer is running and finish = false.
+     */
+    private JButton view_button;
+    
+    /**
+     * This button means that the user finished give the information and
+     * want to send it.
+     */
+    public JButton send_button;
+
+    /**
+     * This button means that the user don't want to send information and
+     * back to normal application.
+     * All the data taken start from user choose to start doing the Feedback sequence 
+     * up to this point will be delete.
+     */
+    private JButton notsend_button; 
+
+    /**
+     * This is the text area where user can explain more about the problem.
+     */
+    private JTextArea problem_details;
+
+    /**
+     * This is the combo box that allow user to specify the poblem type.
+     */
+    private JComboBox problem_type;
+
+    /**
+     * This is the combo box that allow user to specify the urgency of the problem.
+     */
+    private JComboBox problem_urgency;
+
+    /**
+     * This combo box will ask whether or not user want to send the screenshot.
+     */
+    private JComboBox problem_screenshot;
+
+    /**
+     * This text field allow user to type the smtp address of their email that
+     * they want this application to use to send all the data.
+     * If they don't fill this field then it will set to the same
+     * smtp adress of where the data will be send to.
+     */
+    private JTextField smtp_address;
+
+    /**
+     * This text field allow user to type the email adress that they want this
+     * application to use to send all the data.
+     * If they don't fill this field then it will set to the same email
+     * adress of where the data will be send to.
+     */
+    private JTextField email_address;
+
+    /**
+     * This is the name of the window the user lastly used.
+     * This is the name of the window where the user called to start the Feedback
+     * sequence.
+     */
+    private String frameName;
+
+    /**
+     * This is the unique code generated for each time user start doing the Feedback
+     * sequence.
+     * This code will be used as reference to which feedback that the user send.
+     */
+    private static String code;
+
+    /**
+     * This is the date when the user start open this FeedbackInterface form.
+     */
+    private Date curr_date;
+    
+    /**
+     * This is the special listener class that record all the action user did in application.
+     */
+    private static ActionRecorderDialog dialog;
+
+    /**
+     * This variable will hold the resource of the words that is stored in Messages.properties file.
+     * The calling using messages.getString(someString) will caused someString to be translated
+     * into some other string that is hold in that file.usually it will caused it to be translated
+     * to the language that the user use or choose to have as stated in Locale.
+     */
+    private static ResourceBundle messages;
+
+    /**
+     * This is the screen shot instance that will allow the application to take screen shot of the whole
+     * screen or only the window that the user currently doing their action.
+     */
+    private ScreenShot screen;
+
+    /**
+     * This is the thread that allows the encoding of the image file to String and the saving of image to
+     * a jpeg file happens concurrently.
+     */
+    private Thread save;
+
+    /**
+     * This is the hashmap that hold the screen shot and the panel where the screenshot is held.
+     */
+    private HashMap picAndPane;
+
+    /**
+     * This is the panel that hold all the panels containing the screenshots taken.
+     */
+    private JPanel picture_scroll;
+
+    /**
+     * This is how many rows of screenshots panel displayed at the moment.
+     */
+    private int row;
+
+    /**
+     * This is the array list that hold all the screenshots panel displayed.
+     */
+    private ArrayList paneArray;
+
+    /**
+     * This button will remove the last screenshot panels.
+     */
+    private JButton remove;
+
+    /**
+     * This is a reference to feedback interface displayed at the moment.
+     */
+    private FeedbackInterface frame;
+
+    /**
+     * This is the thread to send all the xml files back to the developer.
+     */
+    private Thread sendThread;
+
+    /**
+     * This is the flag to tell whether or not the sending files finished or not.
+     * It is also flag to tell whether or not the viewing files finished or not.
+     */
+    private static boolean finish;
+
+    /**
+     * This is the timer to displayed the progress bar to tell user to wait while
+     * we sending the files or while we generate the xml files to be viewed.
+     * If finish = false then wait cursor,progress bar and wait label will be displayed.
+     * If finish = true then default cursor will be displayed.
+     */
+    private Timer timer;
+
+    /**
+     * Progress bar displayed when user sending or want to view details.
+     */
+    private JProgressBar progressBar;
+
+    /**
+     * Label displayed to tell user to wait for a while.
+     */
+    private JLabel wait_lbl;
+
+    /**
+     * This is the background colour for the window.
+     */
+    private Color bckcolor;
+    
+    /**
+     * This is the panel to displayed all descriptions panel.
+     */
+    private JPanel tempPane;
+
+    /**
+     * This is the thread that will be running when user pressed the preview button.
+     * This thread will show the 6 xml files that are going to be send. It will run concurrently
+     * with the timer, if value of finish = true, this thread will be null-ed.
+     */
+    private Thread prevThread;
+    
+    /**
+     * This is the flag to say whether or not this window should be dispose when timer is stop
+     * and finish = true.
+     * If selected = true then this window should be dispose, otherwise don't dispose it.
+     */
+    private boolean selected = false;
+    
+    /**
+     * This is a blocker to say that user cannot do anything while waiting.
+     */
+    private MouseListener mouse_blocker_listener = new MouseAdapter() {};
+ 
+    /**
+     * Will make a dialog window that allows people to write more about the problem that want
+     * to send.
+     * This is the GUI for user to write on Feedback form. It is also setting the up the appropriate 
+     * data member with the value passed on to this constructor.
+     * (Precondition: (sh != null) && (dlg != null) && (msg != null))
+     * @param sh  an instance of ScreenShot that will allow application to take screenshot.
+     * @param frame_name name of the window where user called Feedback form from.
+     * @param msg holding the resource of the words that is stored in Messages.properties file.
+     * @param dlg holding the special listener.
+     */
+    public FeedbackInterface(ScreenShot sh,String frame_name,
+			     ResourceBundle msg,ActionRecorderDialog dlg)
+    {
+	super();
+
+	frame = this;
+
+	bckcolor = new Color(176,209,217);
+
+	picAndPane = new HashMap();
+
+	messages = msg;
+	frameName = frame_name;
+	dialog = dlg;
+
+	FeedbackInterface.setDefaultLookAndFeelDecorated(true);
+	setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+	setTitle(messages.getString("SendFeedbackForm"));
+	
+	screen = sh;
+	Container container;
+	container = getContentPane();
+	createUI(container);
+	curr_date = new Date();
+	
+	Toolkit kit;
+	kit = Toolkit.getDefaultToolkit();
+	Dimension screenSize;
+	screenSize = kit.getScreenSize();
+	int screenHeight;
+	screenHeight = screenSize.height;
+	int screenWidth;
+	screenWidth = screenSize.width;
+	Dimension size;
+	size = this.getSize();
+	setLocation((screenWidth - size.width - 500) , 
+		    (screenHeight - size.height - 700));
+	
+	setBackground(bckcolor);
+	pack();
+	setModal(false);
+	setVisible(true);
+    }
+
+    /**
+     * This method is setting up the content pane for the Feedback form window.
+     * (Precondition: (window != null))
+     * @param window the content pane of Feedback form window. 
+     */
+    public void createUI(Container window)
+    {
+	JPanel container;
+	container = new JPanel();
+	container.setBackground(bckcolor);
+	container.setLayout(new BoxLayout(container,BoxLayout.PAGE_AXIS));
+	
+	String opening_text;
+	opening_text = messages.getString("FeedbackOpeningText");
+	JTextArea textArea;
+	textArea = new JTextArea(opening_text);
+	textArea.setMinimumSize(new Dimension(100,100));
+	textArea.setOpaque(false);
+	textArea.setEditable(false);
+	textArea.setLineWrap(true);
+	textArea.setWrapStyleWord(true);
+	textArea.setBorder(new EmptyBorder(10,10,0,10));
+	textArea.setBackground(bckcolor);
+	container.add(textArea);
+
+	tempPane = new JPanel();
+	tempPane.setLayout(new BorderLayout());
+	tempPane.setBackground(new Color(176,208,176));
+
+	paneArray = new ArrayList();
+	picture_scroll = new JPanel();
+	row = 1;
+	picture_scroll.setLayout(new GridLayout(0,1));
+	picture_scroll.setBackground(new Color(176,208,176));
+	tempPane.add(picture_scroll,BorderLayout.PAGE_START);
+
+	JPanel btnPanel;
+	btnPanel = new JPanel();
+	btnPanel.setBackground(new Color(176,208,176));
+	remove = new JButton("Remove");
+	remove.setActionCommand("Remove");
+	remove.addActionListener(new ActionListener ()
+	    {
+		public void actionPerformed(ActionEvent e)
+		{
+		    Object obj;
+		    JPanel pane;
+
+		    if (paneArray.size() == 0)
+			System.out.println("There is no panel here!");
+		    else
+			{
+			    pane = (JPanel) paneArray.get((row - 2));
+			    obj = picAndPane.get(pane);
+			    obj = null;
+			    picAndPane.remove(pane);
+			    paneArray.remove((row - 2));
+			    
+			    if (paneArray.size() == 0)
+				remove.setVisible(false);
+
+			    picture_scroll.remove(pane);
+			
+			    row--;
+			    
+			    picture_scroll.setSize(picture_scroll.getPreferredSize());
+			    picture_scroll.revalidate();
+			    picture_scroll.repaint();
+			    tempPane.setSize(tempPane.getPreferredSize());
+			    tempPane.revalidate();
+			    tempPane.repaint();
+			}
+		}
+	    });
+	remove.setToolTipText("Remove last thumbnail");
+	remove.setBackground(new Color(176,208,176));
+	remove.setVisible(false);
+	btnPanel.add(remove);
+	tempPane.add(btnPanel,BorderLayout.PAGE_END);
+   
+	JScrollPane pict_scroll;
+	pict_scroll = new JScrollPane(tempPane);
+	pict_scroll.setBackground(bckcolor);
+	pict_scroll.setPreferredSize(new Dimension(300,300));
+	pict_scroll.setBorder(new EmptyBorder(0,10,10,10));
+	container.add(pict_scroll);
+
+	JPanel pane;
+	pane = new JPanel();
+	pane.setBackground(bckcolor);
+	pane.setLayout(new GridLayout(0,2));
+	pane.setBorder(new EmptyBorder(10,10,10,10));
+		
+	JLabel txt_type;
+	txt_type = new JLabel(messages.getString("Whatkindofproblemisit") + "?    ");
+	txt_type.setBackground(bckcolor);
+	pane.add(txt_type);
+	
+	String [] type = {" ",messages.getString("ContentError"),
+			  messages.getString("ImageNotShow"),
+			  messages.getString("StrangeBehaviour"),
+			  messages.getString("SomethingUnexpectedHappen"),
+			  messages.getString("HardToUse"),
+			  messages.getString("Other")};
+	problem_type = new JComboBox(type);
+	problem_type.setOpaque(false);
+	problem_type.addActionListener(this);
+	problem_type.setBackground(bckcolor);
+	pane.add(problem_type);
+
+	JLabel txt_urgent;
+	txt_urgent = new JLabel(messages.getString("Howbadistheproblem")+"?  ");
+	txt_urgent.setBackground(bckcolor);
+	pane.add(txt_urgent);
+
+	String[] urgent = {" ",messages.getString("Critical"),
+		  messages.getString("Serious"),
+		  messages.getString("Medium"),messages.getString("Minor"),
+		  messages.getString("Trivial")};
+	problem_urgency = new JComboBox(urgent);
+	problem_urgency.setOpaque(false);
+	problem_urgency.addActionListener(this);
+	problem_urgency.setBackground(bckcolor);
+	pane.add(problem_urgency);
+
+	/*JLabel txt_screenshot;
+	txt_screenshot= new JLabel(messages.getString("Doyouwanttosendthescreenshot") + "?  ");
+	txt_screenshot.setBackground(new Color(108,168,108));
+	pane.add(txt_screenshot);
+
+	String[] screenshot = {messages.getString("Yes"),
+			       messages.getString("No")};
+	problem_screenshot = new JComboBox(screenshot);
+	problem_screenshot.addActionListener(this);
+	problem_screenshot.setBackground(new Color(108,168,108));
+	pane.add(problem_screenshot);*/
+	
+	
+	/*JLabel txt_smtp;
+	txt_smtp = new JLabel(messages.getString("SMTPaddress") + "?  ");
+	txt_smtp.setBackground(new Color(176,208,176));
+	pane.add(txt_smtp);
+
+	smtp_address = new JTextField();
+	smtp_address.setBackground(new Color(224,240,224));
+	pane.add(smtp_address);*/
+	
+
+	/*JLabel txt_email;
+	txt_email = new JLabel(messages.getString("Emailaddress") + "?  ");
+	txt_email.setBackground(new Color(176,208,176));
+	pane.add(txt_email);
+
+	email_address = new JTextField();
+	email_address.setBackground(new Color(224,240,224));
+	pane.add(email_address);*/
+	
+	JLabel empty;
+	empty = new JLabel ("   ");
+	empty.setBackground(bckcolor);
+	pane.add(empty);
+	JLabel empty2;
+	empty2 = new JLabel ("   ");
+	empty2.setBackground(bckcolor);
+	pane.add(empty2);
+	container.add(pane);
+
+	JPanel panely;
+	panely = new JPanel();
+	panely.setLayout(new GridLayout(0,3));
+	panely.setBackground(bckcolor);
+	panely.setBorder(new EmptyBorder(new Insets(10,10,10,10)));
+	
+	view_button = new JButton("Details . . .");
+	view_button.setActionCommand(messages.getString("Preview"));
+	view_button.addActionListener(this);
+	view_button.setToolTipText(messages.getString("ConformationPreview"));
+	view_button.setBackground(bckcolor);
+	panely.add(view_button); 
+
+	send_button = new JButton(messages.getString("Send"));
+	send_button.setActionCommand(messages.getString("Send"));
+	send_button.addActionListener(this);
+	send_button.setDefaultCapable(true);
+	send_button.setToolTipText(messages.getString("FeedbackSendButton"));
+	send_button.setBackground(bckcolor);
+	panely.add(send_button);
+	
+	notsend_button = new JButton("Cancel");
+	notsend_button.setActionCommand(messages.getString("NotSend"));
+	notsend_button.addActionListener(this);
+	notsend_button.setToolTipText(messages.getString("FeedbackNotSendButton"));
+	notsend_button.setBackground(bckcolor);
+	panely.add(notsend_button);
+	container.add(panely);
+
+	//Align the left edges of the components.
+	textArea.setAlignmentX(Component.LEFT_ALIGNMENT);
+	pict_scroll.setAlignmentX(Component.LEFT_ALIGNMENT);
+	pane.setAlignmentX(Component.LEFT_ALIGNMENT);
+	panely.setAlignmentX(Component.LEFT_ALIGNMENT);
+
+	String about;
+	about = messages.getString("FeedbackAboutText");
+	JTextArea about_txt;
+	about_txt = new JTextArea(about);
+	about_txt.setEditable(false);
+	about_txt.setLineWrap(true);
+	about_txt.setWrapStyleWord(true);
+	about_txt.setBackground(bckcolor);
+	about_txt.setBorder(new EmptyBorder(10,10,10,10));
+
+	String privacy;
+	privacy = messages.getString("FeedbackPrivacyText");
+	JTextArea privacy_txt;
+	privacy_txt = new JTextArea(privacy);
+	privacy_txt.setEditable(false);
+	privacy_txt.setLineWrap(true);
+	privacy_txt.setWrapStyleWord(true);
+	privacy_txt.setBackground(bckcolor);
+	privacy_txt.setBorder(new EmptyBorder(10,10,10,10));
+ 
+	JTabbedPane tab;
+	tab = new JTabbedPane();
+	tab.setBackground(bckcolor);
+	tab.addTab(messages.getString("Form"),container);
+	tab.addTab(messages.getString("About"),about_txt);
+	tab.addTab(messages.getString("Privacy"),privacy_txt);
+	tab.setBorder(new EmptyBorder(5,5,5,5));
+	
+	JPanel panelx;
+	panelx = new JPanel();
+	panelx.setLayout(new BoxLayout(panelx,BoxLayout.PAGE_AXIS));
+	panelx.setBackground(bckcolor);
+	panelx.setBorder(new EmptyBorder(new Insets(10,10,10,10)));
+	
+	wait_lbl = new JLabel(messages.getString("Pleasewait"));
+	wait_lbl.setBackground(bckcolor);
+	wait_lbl.setVisible(false);
+	panelx.add(wait_lbl);
+	
+	progressBar = new JProgressBar ();
+	progressBar.setBackground(bckcolor);
+	progressBar.setVisible(false);
+	panelx.add(progressBar);
+
+	JLabel emp2;
+	emp2 = new JLabel ("   ");
+	emp2.setBackground(bckcolor);
+	panelx.add(emp2);
+
+	wait_lbl.setAlignmentX(Component.CENTER_ALIGNMENT);
+	progressBar.setAlignmentX(Component.CENTER_ALIGNMENT);
+	emp2.setAlignmentX(Component.CENTER_ALIGNMENT);
+
+	final String p1;
+	final String p2;
+	p1 = "Tab";
+	p2 = "ProgressBar";
+
+	final JPanel cards;
+	cards = new JPanel(new CardLayout());
+	cards.setBackground(bckcolor);
+	cards.add(tab,p1);
+	cards.add(panelx,p2);
+
+	window.add(cards);
+
+	
+	timer = new Timer(1000, new ActionListener()
+	    {
+		public void actionPerformed(ActionEvent evt)
+		{
+		    if (finish == true)
+			{
+			    if (sendThread != null)
+				{
+				    sendThread.interrupt();
+				    sendThread = null;
+				}
+
+			    if (prevThread != null)
+				{
+				    prevThread.interrupt();
+				    prevThread = null;
+				}
+			    
+			    Component glass_pane;
+			    glass_pane = frame.getGlassPane();
+			    
+			    glass_pane.setVisible(false);
+			    glass_pane.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+			    glass_pane.removeMouseListener(mouse_blocker_listener);
+			    
+			    wait_lbl.setVisible(false);
+			    progressBar.setIndeterminate(false);
+			    progressBar.setVisible(false);
+			    
+			    frame.setTitle(messages.getString("SendFeedbackForm"));
+			    
+			    CardLayout cl = (CardLayout)(cards.getLayout());
+			    cl.show(cards,p1);
+			    
+			    if (selected == true)
+				frame.dispose();
+
+			    timer.stop();
+			}
+		    else
+			{
+			    Component glass_pane;
+			    glass_pane = frame.getGlassPane();
+			    
+			    glass_pane.addMouseListener(mouse_blocker_listener);
+			    glass_pane.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+			    glass_pane.setVisible(true);
+
+			    frame.setTitle("Please wait...");
+
+			    CardLayout cl = (CardLayout)(cards.getLayout());
+			    cl.show(cards,p2);
+
+			    wait_lbl.setVisible(true);
+			    progressBar.setIndeterminate(true);
+			    progressBar.setVisible(true);
+			}
+		}
+	    });
+    }
+
+    /**
+     * This method will set the finish variable values.
+     * When the sending files is finished, the sending application  will set the finish value to true.
+     * @param fin whether or not the send files is finished.
+     */
+    public static void setFinish (boolean fin)
+    {
+	finish = fin;
+    }
+
+    /**
+     * This is the class to represent the whole panel that hold the screenshot and the comments
+     * about the screenshot.
+     */
+    private class ThumbnailPic implements Serializable
+    {
+	/**
+	 * This is the panel that hold a thumbnail for a screenshot and a comments text area.
+	 */
+	private JPanel picpane;
+
+	/**
+	 * This hold the BufferedImages for the whole screen version of screenshot.
+	 */
+	private BufferedImage[] screenimages;
+
+	/**
+	 * This hold the BufferedImages for the window version only.
+	 */
+	private BufferedImage[] windowimages;
+
+	/**
+	 * This is the text area where user can enter their comments.
+	 */
+	private JTextArea problem_details;
+
+	/**
+	 * This is a flag to tell us which image user wants to send.Its defaults value is false.
+	 * If iswindow = true then it means user wants to send the window version only.
+	 * If iswindow = false then it means user wants to send the whole screen version.
+	 */
+	private boolean iswindow;
+
+	/**
+	 * This is the titles for the panel.
+	 * Usually this is the title of the window where user wants to do the screenshot.
+	 * If the title is more than 40 characters then we will trim it and add ... at the back of it.
+	 */
+	private String titles;
+
+	/**
+	 * This is the width of the image user wants to send.
+	 */
+	private String iw;
+
+	/**
+	 * This is the height of the image user wants to send.
+	 */
+	private String ih;
+
+	/**
+	 * This is the x-coordinate where the window located inside the whole screen image.
+	 */
+	private int xcoord;
+
+	/**
+	 * This is the y-coordinate where the window located inside the whole screen image.
+	 */
+	private int ycoord;
+
+	/**
+	 * This is the width of the window inside the whole screen image.
+	 */
+	private int width;
+
+	/**
+	 * This is the height of the window inside the whole screen image.
+	 */
+	private int height;
+
+	/**
+	 * This is the constructor that will setup the panel that contains
+	 * a thumbnail screenshot and a comments text area.
+	 * @param image the whole screen screenshot image.
+	 * @param text  the title for this pane.
+	 * @param x     the x-coordinate of the window inside the whole screen image.
+	 * @param y     the y-coordinate of the window inside the whole screen image.
+	 * @param w     the width of the window inside the whole screen image.
+	 * @param h     the height of the window inside the whole screen  image. 
+	 */
+	//public ThumbnailPic (BufferedImage screen,byte[] image,String text)
+	public ThumbnailPic (BufferedImage image,String text,int x,int y,int w,int h)
+	{
+	    xcoord = x;
+	    ycoord = y;
+	    width = w;
+	    height = h;
+	    iswindow = false;
+	    titles = text;
+	    screenimages = new BufferedImage[3];
+	    windowimages = new BufferedImage[3];
+	    //screenimages[0] = screen;
+	    screenimages[0] = image;
+	    screenimages[1] = null;
+	    screenimages[2] = null;
+	    windowimages[0] = null;
+	    windowimages[1] = null;
+	    windowimages[2] = null;
+	    addPanel(image);
+	}
+
+	/**
+	 * This method will setup the components inside the panel properly.
+	 * @param image the whole screen screenshot image.
+	 */
+	//public void addPanel (byte[] image)
+	public void addPanel (BufferedImage image)
+	{
+	    picpane = new JPanel();
+	    picpane.setLayout(new BorderLayout());
+	    
+	    final JButton picture;
+	    picture = new JButton();
+
+	    int w;
+	    int h;
+
+	    w = (int) (150/2);
+	    h = (int) (150/2);
+	    
+	    iw = "" + image.getWidth();
+	    ih = "" + image.getHeight();
+
+	    ImageIcon thumbnail;
+	    thumbnail = new ImageIcon(image.getScaledInstance(w,h,Image.SCALE_SMOOTH));
+
+	    picture.setIcon(thumbnail);
+	    picture.setText("Draw . . .");
+	    picture.setActionCommand("Scribble");
+	    picture.addActionListener(new ActionListener()
+		{
+		    public void actionPerformed (ActionEvent e)
+		    {
+			frame.addMouseListener(mouse_blocker_listener);
+			frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+			SelectPicture select;
+			select = new SelectPicture(screenimages,windowimages,iswindow,problem_details.getText(),messages);
+			frame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+			frame.removeMouseListener(mouse_blocker_listener);	
+			select.setWindowBounds(xcoord,ycoord,width,height);
+			
+			iswindow = select.getIsWindow();
+
+			screenimages[0] = select.getImage3();
+			screenimages[1] = select.getImage();
+			screenimages[2] = select.getImage2();
+
+			if (iswindow == true)
+			    {
+				windowimages[0] = select.getWindowImage3();
+				windowimages[1] = select.getWindowImage();
+				windowimages[2] = select.getWindowImage2();
+			    }
+			else
+			    {
+				windowimages[0] = null;
+				windowimages[1] = null;
+				windowimages[2] = null;
+			    }
+			
+			problem_details.setText(select.getDetails());
+
+			int w,h;
+			w = (int) (150/2);
+			h = (int) (150/2);
+			
+			ImageIcon thumbnail;
+
+			if (iswindow == false)
+			    thumbnail = new ImageIcon(screenimages[1].getScaledInstance(w,h,Image.SCALE_SMOOTH));
+			else
+			    thumbnail = new ImageIcon(windowimages[1].getScaledInstance(w,h,Image.SCALE_SMOOTH));
+
+			picture.setIcon(thumbnail);
+			
+			if (select.getSendNow() == true)
+			    {
+				ActionEvent evt;
+				evt = new ActionEvent(send_button,ActionEvent.ACTION_PERFORMED,messages.getString("Send"));
+
+				frame.actionPerformed(evt);
+			    }
+		    }
+		});
+	    picture.setToolTipText("Click if you want to draw on it");
+	    picture.setMargin(new Insets(5,5,5,5));
+	    picture.setVerticalTextPosition(AbstractButton.BOTTOM);
+	    picture.setHorizontalTextPosition(AbstractButton.CENTER);
+	    picture.setBackground(new Color(176,208,176));
+	    picpane.add(picture,BorderLayout.LINE_START);
+	    
+	    JPanel txtPane;
+	    txtPane = new JPanel(new BorderLayout());
+	    txtPane.setBackground(new Color(176,208,176));
+	    txtPane.setBorder(new EmptyBorder(5,5,5,5));
+
+	    JLabel lbldesc = new JLabel("Comments :");
+	    lbldesc.setBackground(new Color(176,208,176));
+	    txtPane.add(lbldesc,BorderLayout.PAGE_START);
+
+	    problem_details = new JTextArea();
+	    problem_details.setWrapStyleWord(true);
+	    problem_details.setEditable(true);
+	    problem_details.setBackground(new Color(224,240,224));
+	   
+	    JScrollPane scroll;
+	    scroll = new JScrollPane(problem_details);
+	    scroll.setBackground(new Color(176,208,176));
+	    scroll.setPreferredSize(new Dimension(300,150));
+	    txtPane.add(scroll,BorderLayout.LINE_START);
+	   
+	    picpane.setSize(new Dimension(150,150));
+	    picpane.add(txtPane,BorderLayout.CENTER);
+	}
+
+	/**
+	 * This method will get the width of the image user wants to send.
+	 * @return the width of the image.
+	 */
+	public String getWidth()
+	{
+	    if (iswindow == false)
+		return "" + screenimages[0].getWidth();
+	    else
+		{
+		    return "" + windowimages[0].getWidth();
+		}
+	}
+
+	/**
+	 * This method will get the height of the image user wants to send.
+	 * @return the height of the image.
+	 */
+	public String getHeight()
+	{
+	    if (iswindow == false)
+		return "" + screenimages[0].getHeight();
+	    else
+		{
+		    return "" + windowimages[0].getHeight();
+		}  
+	}
+
+	/**
+	 * This method will get the titles of this panel.
+	 * @return the panel's title.
+	 */
+	public String getTitles()
+	{
+	    return titles;
+	}
+
+	/**
+	 * This method will get user comments inside the text area.
+	 * @return the user's comments.
+	 */
+	public String getProblemDetails ()
+	{
+	    String txt;
+	    txt = problem_details.getText();
+	    
+	    if (txt == null)
+		return " ";
+	    else
+		return txt;
+	}
+
+	/**
+	 * This method will setting up the text inside the text area.
+	 * @param comments the text set to the text area.
+	 */
+	public void setComments (String comments)
+	{
+	    problem_details.setText(comments);
+	}
+
+	/**
+	 * This method will get the image that user wants to send.
+	 * @return image going to be send.
+	 */
+	public BufferedImage[] getImages ()
+	{
+	    if (iswindow == false)
+		return screenimages;
+	    else
+		return windowimages;
+	}
+
+	/**
+	 * This method will get the non-scribble lines image that user wants to send.
+	 * @return the string representation of the non-scribble lines image.
+	 */
+	public String getScreenImage ()
+	{
+	    if (iswindow == false)
+		return convert(screenimages[0]);
+	    else
+		return convert(windowimages[1]);
+	}
+
+	/**
+	 * This method will get the scribble lines image that user wants to send.
+	 * @return the string representation of the scribble lines image.
+	 */
+	public String getScreenAndLineImage ()
+	{
+	    if (iswindow == false)
+		{
+		    if (screenimages[1]!= null)
+			return convert(screenimages[1]);
+		    else
+			return convert(screenimages[0]);
+		}
+	    else
+		{
+		    if (windowimages[1]!= null)
+			return convert(windowimages[1]);
+		    else
+			return convert(windowimages[0]);
+		}
+	}
+
+	/**
+	 * This method will get only scribble lines image that user wants to send.
+	 * @return the string representation of the only scribble lines image.
+	 */
+	public String getLineForScreenImage ()
+	{
+	    if (iswindow == false)
+		{
+		    if (screenimages[2] != null)
+			return convert(screenimages[2]);
+		    else
+			return "";
+		}
+	    else
+		{
+		    if (windowimages[2] != null)
+			return convert(windowimages[2]);
+		    else
+			return "";
+		}
+	}
+
+	/**
+	 * This method will get the panel that hold a thumbnail and a text area.
+	 * @return panel that represent the thumbnail and the text area.
+	 */
+	public JPanel getPanel()
+	{
+	    return picpane;
+	}
+    }
+
+    /**
+     * This method will setting up the comments to be displayed inside the latest added panel.
+     * So if the window where user wants to take the screenshot at the moment is a modal window
+     * then it will setting the comments to ask user to close the window before they can enter
+     * their comments there.
+     * @param modal the modality of the window where user wants to take the screenshot.
+     */
+    public void setComment (boolean modal)
+    {
+	if (paneArray.size() == 0)
+	    System.out.println("There is no panel here!");
+	else
+	    {
+		JPanel pane;
+		pane = (JPanel) paneArray.get((row - 2));
+		ThumbnailPic obj;
+		obj = (ThumbnailPic) picAndPane.get(pane);
+		String text;
+		if (modal == true)
+		    text = "Please close '" + obj.getTitles() + "' window \nbefore you can enter comments here.";
+		else
+		    text = " ";
+		obj.setComments(text);
+	    }
+    }
+
+    /**
+     * This method will add a panel that contains the thumbnail button and the text area.
+     * @param title the window's title.
+     * @param image the whole screen image.
+     * @param x     the window's x-coordinate in the image.
+     * @param y     the window's y-coordinate in the image.
+     * @param width the window's width in the image.
+     * @param height the window's height in the image.
+     */
+    //public void addScreenPanel (String title,BufferedImage screen,byte[] image)
+    public void addScreenPanel (String title,BufferedImage image,int x,int y,int width,int height)
+    {
+	final JPanel pane;
+	pane = new JPanel();
+	pane.setLayout(new BorderLayout());
+	pane.setBackground(new Color(176,208,176));
+
+	Border loweredetched;
+	loweredetched = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED);
+
+	String ttl;
+	if (title.length() > 41)
+	    ttl = title.substring(0,40) + "  . . . ";
+	else
+	    ttl = title;
+
+	TitledBorder border;
+	border = BorderFactory.createTitledBorder(loweredetched, title);
+	border.setTitleJustification(TitledBorder.LEFT);
+	
+	pane.setBorder(BorderFactory.createCompoundBorder(new EmptyBorder(5,5,5,5),border));
+	
+	ThumbnailPic pic;
+	//pic = new ThumbnailPic(screen,image,title);
+	pic = new ThumbnailPic(image,title,x,y,width,height);
+	picAndPane.put(pane,pic);
+	pane.add(pic.getPanel(),BorderLayout.LINE_START);
+
+	remove.setVisible(true);
+	picture_scroll.add(pane);
+	paneArray.add(pane);
+	row++;
+	picture_scroll.setSize(picture_scroll.getPreferredSize());
+	picture_scroll.revalidate();
+	picture_scroll.repaint();
+	tempPane.setSize(tempPane.getPreferredSize());
+	tempPane.revalidate();
+	tempPane.repaint();
+    }
+
+    /**
+     * This method will generate the unique code that will be used as the 
+     * reference number of the feedback that user are about to send.
+     * @return the unique code.
+     */
+    public String getPrevData ()
+    {
+	try
+	    {
+		UID id = new UID();
+		code =  "" + InetAddress.getLocalHost().hashCode() + id.hashCode();
+	    }
+	catch (UnknownHostException exp) {}
+	return code;
+    }
+
+    /**
+     * This method allows another class to get the unique code that can be used as 
+     * the reference number of the feedback that user are about to send.
+     * This method should be called only if 
+     * getPrevData() method already been called before.Otherwise it will return null. 
+     * @return the unique code
+     */
+    public static String getCode()
+    {
+	return code;
+    }
+
+    /**
+     * This method will encode BufferedImage into their String representation using
+     * Base64 encoder.
+     * (Precondition: (image != null))
+     * @param image is the image that we want to encode to String.
+     */
+    public String convert (BufferedImage image)
+    {
+	String txt;
+	txt = null;
+
+	try
+	    {
+		ByteArrayOutputStream stream = new ByteArrayOutputStream();
+		ImageIO.write(image,"jpg",stream);
+		byte[] bytes = stream.toByteArray();
+
+		txt = Base64.encodeBytes(bytes);
+		
+	    }
+	catch(IOException ex) {}
+
+	return txt;
+    }
+
+    /**
+     * This method is to make sure that there is no thread running at the moment.
+     */
+    private void ensureEventThread()
+    {
+	if(SwingUtilities.isEventDispatchThread())
+	    {
+		return;
+	    }
+	throw new RuntimeException(messages.getString("file"));
+    }
+
+    /**
+     * This method will create all 6 xml files, jarred them into one file, deletes all xml files and
+     * sending it.
+     * The xml files for:
+     * <br> 1.The history & information of all the actions done and windows opened by the application
+     *        after user choose to start the sequence of sending feedback.
+     * <br> 2.The history of all the commands (actions) done by the user before choosing
+     *        after start the sequence of sending feedback.
+     * <br> 3.The informations we get from the feedback interface form period.
+     * <br> will be getting from vector and the temp_feedbackhist.log file.
+     * (Precondition: (sx != null) && (vector != null) && (err_array != null))
+     * @param sx the xml parser.
+     * @param vector the vector that hold the history and information.
+     * @param err_array the arrays of information we take from the feedback interface period.
+     * @param imgFile   the arrays of information we take from the panels inside feedback interface form.
+     */
+    private void sendState (SaveToXML sx,Vector vector,String[] err_array,String[] imgFile)
+    {
+	String dirname;
+	dirname = "xmlfeedback/";
+	
+	File dir = new File("xmlfeedback");
+	if (dir.isDirectory() == false)
+	    dir.mkdir();
+
+	sx.saveFeedback(err_array,imgFile);
+	
+	sx.open(dirname + "feedbackcontent" + FeedbackInterface.getCode() + ".xml",
+		"HISTORY");
+	SaveToXML sx2;
+	sx2 = new SaveToXML(messages);
+	sx2.open(dirname + "feedbackcommand" + FeedbackInterface.getCode() + ".xml",
+		 "COMMANDS");
+	
+	for (int i = 0 ; i < vector.size() ; i++)
+	    {
+		History log;
+		log = (History) vector.get(i);
+		log.sendXML(sx);
+		log.sendXMLComm(sx2);
+	    }
+	
+	Vector stack;
+	stack = getPrev_log();
+	if (stack != null)
+	    {
+		for (int i = 0 ; i < stack.size() ; i++)
+		    {
+			History log;
+			log = (History) stack.get(i);
+			log.sendXML(sx);
+			log.sendXMLComm(sx2);
+		    }
+		stack.removeAllElements();
+		System.gc();
+		stack = null;
+	    }
+	
+	sx2.close("COMMANDS");
+	sx.close("HISTORY");
+	
+	getHistPrev_log(sx,sx2);
+	
+	ZipFile zip;
+	zip = new ZipFile();
+	zip.sendZipXMLFile();
+
+	File f;
+	f = new File (dirname + "feedback" + FeedbackInterface.getCode() + ".xml");
+	f.delete();
+	f = new File (dirname + "feedbackcontent" + FeedbackInterface.getCode() + ".xml");
+	f.delete();
+	f = new File (dirname + "feedbackcommand" + FeedbackInterface.getCode() + ".xml");
+	f.delete();
+	/*f = new File (dirname + "content" + FeedbackInterface.getCode() + ".xml");
+	  f.delete();*/
+	f = new File (dirname + "command" + FeedbackInterface.getCode() + ".xml");
+	f.delete();
+	
+	String[] args = new String[5];
+	args[0] = "vl6@cs.waikato.ac.nz";
+	args[1] = dirname + "Jar" + FeedbackInterface.getCode() + "File.jar";
+	args[4] = "lib/parser.jar";
+	args[2] = err_array[19];
+	args[3] = err_array[20];
+	SendHTTP sm = new SendHTTP();
+	//SendMail sm = new SendMail();
+	sm.sendMail(args,messages,FeedbackInterface.getCode());
+    }
+	
+    /**
+     * This method will create the 2 xml files.
+     * The xml files are:
+     * <br> 1.The history & information of all the actions done and windows opened by the application
+     *         before user choose to start the sequence of sending feedback.
+     * <br> 2.The history of all the commands (actions) done by the user before choosing
+     *         to start the sequence of sending feedback.
+     * <br> The information will be getting from history.log file and the temp_history.log file.
+     * (Precondition: (sx != null) && (sx2 != null))
+     * @param sx the xml parser for the history xml file.
+     * @param sx2 the xml parser for the command xml file.
+     */
+    private void getHistPrev_log(SaveToXML sx,SaveToXML sx2)
+    {
+	Vector stack = null;
+	try
+	    {
+		File f = new File("history.log");
+		if (f.exists() == true)
+		    {
+			FileInputStream fis = new FileInputStream(f);
+			ObjectInputStream ois = new ObjectInputStream(fis);
+			
+			stack = (Vector) ois.readObject();
+			
+			String dirname;
+			dirname = "xmlfeedback/";
+
+			/*sx.open(dirname + "content" + FeedbackInterface.getCode() + ".xml",
+			  "HISTORY");*/
+			sx2.open(dirname + "command" + FeedbackInterface.getCode() + ".xml",
+				 "COMMANDS");
+			
+			if (stack != null)
+			    {
+				for (int i = 0 ; i < stack.size() ; i++)
+				    {
+					History log;
+					log = (History) stack.get(i);
+				        //log.sendXML(sx);
+					log.sendXMLComm(sx2);
+				    }
+				stack.removeAllElements();
+				System.gc();
+				stack = null;
+			    }
+			
+			stack = (Vector) ois.readObject();
+			
+			if (stack != null)
+			    {
+				for (int i = 0 ; i < stack.size() ; i++)
+				    {
+					History log;
+					log = (History) stack.get(i);
+				        //log.sendXML(sx);
+					log.sendXMLComm(sx2);
+				    }
+				stack.removeAllElements();
+				System.gc();
+				stack = null;
+			    }
+			
+			sx2.close("COMMANDS");
+			//sx.close("HISTORY");
+			
+			ois.close();
+		    }
+	    }
+	catch (IOException exp) {exp.printStackTrace();}
+	catch (ClassNotFoundException exp2)
+	    {
+		System.out.println("class exp");}
+    }
+
+    /**
+     * This method will get the Vector stored in temp_feedbackhist.log file.
+     * @return the vector stored in temp_feedbackhist.log file.
+     */
+    public Vector getPrev_log()
+    {
+	Vector stack = null;
+	try
+	    {
+		File f = new File("temp_feedbackhist.log");
+		if (f.exists() == true)
+		    {
+			FileInputStream fis = new FileInputStream(f);
+			ObjectInputStream ois = new ObjectInputStream(fis);
+			stack = (Vector) ois.readObject();
+			ois.close();
+			return stack;
+		    }
+		else
+		    stack = null;
+	    }
+	catch (IOException exp) {exp.printStackTrace();}
+	catch (ClassNotFoundException exp2)
+	    {
+		System.out.println("class exp");}
+	
+	return stack;
+    }
+
+    /**
+     * This is the thread that will start when user click send button and it will
+     * make 6 xml files,jarred them together into 1 jar file, deletes the 6 xml files and send it.
+     * @param vector the vector stored the current actions history.
+     * @param err_array the array that stored all the information took during feedback interface period.
+     * @param imgFile the array of that stored all the information from all the panels inside feedback interface form.
+     */
+    public void sendMethod (final Vector vector,final String err_array[], final String imgFile[])
+    {
+	ensureEventThread();
+	Runnable sendRun;
+	sendRun = new Runnable()
+	    {
+		public void run ()
+		{
+		    finish = false;
+		    selected = true;
+		    SaveToXML sx;
+		    sx = new SaveToXML(messages);
+		    sendState(sx,vector,err_array,imgFile);
+		    finish = true;
+		}
+	    };
+	sendThread = new Thread(sendRun);
+	sendThread.start();
+    }
+
+    /**
+     * This method will called the window that will display the preview of all
+     * the file going to be send.
+     * @param vector the vector stored the current actions history.
+     * @param err_array the array that stored all the information took during feedback interface period.
+     * @param imgFile the array of that stored all the information from all the panels inside feedback interface form.
+     */
+    public void viewMethod (final Vector vector,final String err_array[], final String imgFile[])
+    {
+	ensureEventThread();
+	Runnable prevRun;
+	prevRun = new Runnable()
+	    {
+		public void run ()
+		{
+		    finish = false;
+		    selected = false;
+		    ReportDetails rd;
+		    rd = new ReportDetails(vector,err_array,imgFile,messages,true);
+		    finish = true;
+		}
+	    };
+	
+	prevThread = new Thread(prevRun);
+	prevThread.start();
+    }
+
+    /**
+     * This method will save a BufferedImage to a jpeg file with the specified filename.
+     * This method is a thread that will caused the thread save to be started and stop
+     * if the saving process is finished.
+     * (Precondition: (I != null))
+     * @param I is the BufferedImage that we want to save.
+     * @param picScreen name of the jpeg file where we want to store the BufferedImage
+     */
+    public void saveGraph(final BufferedImage I,final String picScreen)
+    {
+	ensureEventThread();
+	Runnable saveRun;
+	saveRun = new Runnable ()
+	    {
+		public void run()
+		{
+		    try 
+			{
+			    File file;
+			    file = new File(picScreen);
+			    ImageIO.write(I, "jpg", file);
+			    save = null;
+			} 
+		    catch (Exception ex) {}
+		}
+	    };
+	
+	save = new Thread(saveRun);
+	save.start();
+    }
+
+    /**
+     * This method will get all the informations from the all the panels inside the feedback interface form.
+     * @return the arrays of information from all the panels.
+     */
+    public String[] getimgFile()
+    {
+	String[] imgFile;	
+	imgFile = null;
+
+	/*String answer;
+	  answer = (String) problem_screenshot.getSelectedItem();
+	  if(answer == messages.getString("Yes"))
+	  {*/
+	
+	if (paneArray.size() > 0)
+	    {
+		imgFile = new String[(paneArray.size() * 10)];
+		
+		int step;
+		for (step = 0 ; step < (paneArray.size()*10) ; step=step+10)
+		    {
+			JPanel pane;
+			ThumbnailPic obj;
+			pane = (JPanel) paneArray.get((step/10));
+			obj = (ThumbnailPic) picAndPane.get(pane);
+			
+			//obj.setSize();
+			
+			String title;
+			title = obj.getTitles();
+			
+			imgFile[step+0] = title;
+			imgFile[step+1] = obj.getScreenImage();
+			title = "Window" + pane.hashCode() + "File";
+			imgFile[step+2] = messages.getString("screen")+title+step+".jpg";
+			imgFile[step+3] = obj.getScreenAndLineImage();
+			imgFile[step+4] = messages.getString("ErrorLineAnd")+imgFile[step+2];
+			imgFile[step+5] = obj.getLineForScreenImage();
+			imgFile[step+6] = messages.getString("ErrorLineFor")+imgFile[step+2];
+			imgFile[step+7] = obj.getWidth();
+			imgFile[step+8] = obj.getHeight();
+			imgFile[step+9] = obj.getProblemDetails();
+		    }
+	    }
+		
+	return imgFile;
+    }
+
+    /**
+     * This method will get all the informations taken during feedback interface period, such as os version.
+     * @return then arrays of informations.
+     */
+    public String[] geterr_array()
+    {
+	String[] err_array;
+	err_array = null;
+
+	String os_name;
+	os_name = System.getProperty("os.name") + " " + System.getProperty("os.version") 
+	    + " " + System.getProperty("os.arch");
+	
+	Date problem_date;
+	String err_date,java_info,screen_res;
+	problem_date = new Date();
+	err_date = problem_date.toString();
+	java_info = System.getProperty("java.vendor")+ " " + System.getProperty("java.class.version");
+	Dimension dim;
+	dim = Toolkit.getDefaultToolkit().getScreenSize();
+	screen_res = "" + dim.width + "x" + dim.height;
+	
+	try
+	    {
+		err_array = new String[24];
+
+		err_array[0] = code;
+		err_array[1] = frameName;
+		err_array[2] = "";
+		err_array[3] = (String) problem_type.getSelectedItem();
+		err_array[4] = (String) problem_urgency.getSelectedItem();
+		err_array[5] = System.getProperty("user.name");
+		err_array[6] = System.getProperty("user.home");
+		err_array[7] = System.getProperty("user.dir");
+		err_array[8] = curr_date.toString();
+		err_array[9] = err_date;
+		err_array[10] = os_name;
+		err_array[11] = java_info;
+		err_array[12] = "" + Locale.getDefault();
+		err_array[13] = Gatherer.PROGRAM_NAME + " " + Gatherer.PROGRAM_VERSION; 
+		err_array[14] = InetAddress.getLocalHost().getHostName();
+		err_array[15] = InetAddress.getLocalHost().getHostAddress();
+		err_array[16] = screen_res;
+		err_array[17] = "";
+		
+		/*String test_txt;
+		  test_txt = smtp_address.getText();
+		  if ((test_txt == null)||(test_txt.length() < 5))*/
+		err_array[19] = "mail.waikato.ac.nz";
+		/*else
+		  err_array[19] = test_txt;
+		  
+		  test_txt = email_address.getText();
+		  if ((test_txt == null)||(test_txt.length() < 5))*/
+		err_array[20] = "vl6@cs.waikato.ac.nz";
+		//else
+		//err_array[20] = test_txt;
+		
+		Runtime run;
+		run = Runtime.getRuntime();
+		err_array[21] = formatFileSize(run.totalMemory());
+		err_array[22] = formatFileSize(run.maxMemory());
+		err_array[23] = formatFileSize(run.freeMemory());
+	    }
+	catch(UnknownHostException exp) {}
+
+	return err_array;
+    }
+
+    /**
+     * This method will format the file size given into more elegant form,e.g. 2KB
+     * @param length the file size.
+     * @return the more elegant string representations of the file size.
+     */
+    public String formatFileSize(long length) 
+    {
+	String BYTE_SUFFIX;
+	BYTE_SUFFIX = " B";
+	long GIGABYTE;
+	GIGABYTE = 1024000000l;
+	String GIGABYTE_SUFFIX;
+	GIGABYTE_SUFFIX = " GB";
+	long KILOBYTE;
+	KILOBYTE = 1024l;
+	String KILOBYTE_SUFFIX;
+	KILOBYTE_SUFFIX = " KB";
+	long MEGABYTE;
+	MEGABYTE = 1024000l;
+	String MEGABYTE_SUFFIX;
+	MEGABYTE_SUFFIX = " MB";
+
+	StringBuffer result;
+	result = new StringBuffer("");
+	float number;
+	number = 0f;
+	String suffix;
+	suffix = null;
+	
+	if(length >= GIGABYTE) 
+	    {
+		number = (float) length / (float) GIGABYTE;
+		suffix = GIGABYTE_SUFFIX;
+	    }
+	else if(length >= MEGABYTE) 
+	    {
+		number = (float) length / (float) MEGABYTE;
+		suffix = MEGABYTE_SUFFIX;
+	    }
+	else if(length >= KILOBYTE) 
+	    {
+		number = (float) length / (float) KILOBYTE;
+		suffix = KILOBYTE_SUFFIX;
+	    }
+	else 
+	    {
+		return length + BYTE_SUFFIX;
+	    }
+	
+	String number_str;
+	number_str = Float.toString(number);
+	char number_char[] = number_str.toCharArray();
+	int pos,i;
+	pos = 0;
+	
+	while(number_char != null && pos < number_char.length && number_char[pos] != '.')
+	    {
+		result.append(number_char[pos]);
+		pos++;
+	    }
+
+	if(pos < number_char.length) 
+	    {
+		result.append(number_char[pos]);
+		pos++;
+		
+		for(i = 0; i < 2 && pos < number_char.length; i++, pos++)
+		    result.append(number_char[pos]);
+	   
+		while(pos < number_char.length && number_char[pos] != 'E') 
+		    pos++;
+	   
+		while(pos < number_char.length) 
+		    {
+			result.append(number_char[pos]);
+			pos++;
+		    }
+	    }
+	
+	result.append(suffix);
+	
+	return result.toString();
+    }
+
+    /**
+     * This method will decide what action should be done if the user entered a button in
+     * the Feedback form.
+     * <br>If user click cancel button then all the information taken in this form will be deleted
+     * and user will be quitted from the Reporting Feedback sequence.
+     * <br>If user click the send button then all the information taken will be stored in array which
+     * then will be saved into xml files and sent when user confirm to send all the files.
+     * <br>If user click the details button then they can view all the xml files that are going to be send.
+     * @param e the button that user clicked
+     */
+    public void actionPerformed (ActionEvent e)
+    {
+	getPrevData();
+
+	if(messages.getString("Send").equals(e.getActionCommand()))
+	    {
+		String[] imgFile;
+		String[] err_array;
+
+		imgFile = getimgFile();
+		err_array = geterr_array();
+
+		timer.start();
+		
+		sendMethod(dialog.getVector(),err_array,imgFile);
+	    }
+
+	if(messages.getString("Preview").equals(e.getActionCommand()))
+	    {
+		String[] imgFile;
+		String[] err_array;
+
+		imgFile = getimgFile();
+		err_array = geterr_array();
+
+		timer.start();
+		
+		viewMethod(dialog.getVector(),err_array,imgFile); 
+	    }
+
+	if(messages.getString("NotSend").equals(e.getActionCommand()))
+	    {
+		dispose();
+	    }
+    }
+}
+	
+	
+
+	
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Graphs.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Graphs.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Graphs.java	(revision 31635)
@@ -0,0 +1,360 @@
+package org.greenstone.gatherer.feedback;
+
+import java.awt.image.*;
+import java.awt.geom.*;
+import java.awt.geom.Line2D.*;
+import java.awt.geom.Line2D.Double;
+import java.io.*;
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * This is the class that will allow user to draw a same line to 2 different BufferedImage
+ * at the same time.
+ * The first BufferedImage is BufferedImage with some Image as the based and then
+ * we draw a line on it, and the second BufferedImage is just a transparent Image and then we 
+ * draw a line on it. The line drawn in first BufferedImage is exactly the same with the line
+ * drawn in second BufferedImage.The Image drawn in the first bufferedimage is the
+ * screen shot image of the whole screen.
+ * @author Veronica Liesaputra
+ */
+public class Graphs
+{ 
+    /**
+     * This is the first buffered image.
+     * It will drawn with an image first to be its based, and a line  
+     * that are identical to line drawn the second BufferedImage, will be drawn on it.
+     */
+    private BufferedImage I = null;
+
+    /**
+     * This is the second buffered image.
+     * Its a transaparent buffered image and a line that are indetical to line
+     * drawn in the first BufferedImage, will be drawn on it. 
+     */
+    private BufferedImage I2;
+
+    /**
+     * This is the Graphics for the first BufferedImage.
+     * This will allow user to draw a line and an image on the first buffered image.
+     */
+    private Graphics2D G;
+    
+    /**
+     * This is the Graphics for the second BufferedImage.
+     * This will allow user to draw a line on the second buffered image.
+     */
+    private Graphics2D G2;
+
+    /**
+     * This is the arrays of the 3 BufferedImage.
+     */
+    private BufferedImage[] screen;
+
+    /**
+     * This is the graphics for screen[0] BufferedImage.
+     * This will allow user to draw something on the screen[0] image.
+     */
+    private Graphics2D G3;
+  
+    /**
+     * This is the width of the image.
+     */
+    private double imwidth;
+
+    /**
+     * This is the height of the image.
+     */
+    private double imheight;
+
+    /**
+     * This is the constructor that will initialised the BufferedImage and
+     * its Graphics.
+     * (Precondition: (sh != null))
+     * @param sh is the images.
+     */
+    public Graphs (BufferedImage[] sh)
+    {
+	screen = sh;
+
+	if (screen[0] != null)
+	    { 
+		imwidth = screen[0].getWidth();
+		imheight = screen[0].getHeight();
+
+
+		if((screen[1] == null)&& (screen[2] == null))
+		    {
+			I = new BufferedImage(screen[0].getWidth(),screen[0].getHeight(),
+					      BufferedImage.TYPE_INT_RGB);
+			I2 = new BufferedImage(I.getWidth(),I.getHeight(),
+					       BufferedImage.TYPE_INT_ARGB);
+		    }
+		else
+		    {
+			I = screen[1];
+			I2 = screen[2];
+		    }
+		start();
+	    }
+	else
+	    System.out.println("weird");
+    }
+
+    /**
+     * This method will make sure that everything is null-ed and dispose properly.
+     */
+    public void flush()
+    {
+	G.dispose();
+	G = null;
+	G3.dispose();
+	G3 = null;
+	G2.dispose();
+	G2 = null;
+
+	int i;
+	for ( i = 0 ; i < 3; i++)
+	    {
+		if (screen[i] != null)
+		    {
+			screen[i].flush();
+			screen[i] = null;
+		    }
+	    }
+
+	I.flush();
+	I = null;
+	I2.flush();
+	I2 = null;
+	System.gc();
+    }
+
+    /**
+     * This method will cleared the scribble lines in the images.
+     */
+    public void reset()
+    {
+	I.flush();
+	I = null;
+	System.gc();
+	I2.flush();
+	I2 = null;
+	G.dispose();
+	System.gc();
+	G2.dispose();
+	G = null;
+	G2 = null;
+	System.gc();
+
+	I = new BufferedImage(screen[0].getWidth(),
+			      screen[0].getHeight(),
+			      BufferedImage.TYPE_INT_RGB);
+	I2 = new BufferedImage(I.getWidth(),I.getHeight(),
+			       BufferedImage.TYPE_INT_ARGB);
+
+	screen[1] = null;
+	screen[2] = null;
+
+	start();
+    }
+    /**
+     * This method will setup the 3 Graphics for the 3 BufferedImage.
+     * Here the graphics for the bufferedimage will be created and the appropriate image
+     * will be drawn to the first bufferedimage.  
+     */
+    public void start ()
+    {
+	G3 = screen[0].createGraphics();
+	G3.drawImage((Image) screen[0],0,0,new JLabel());
+
+	G = I.createGraphics();
+	if (screen[1] == null)
+	    G.drawImage((Image) screen[0],0,0,new JLabel());
+	else
+	    G.drawImage ((Image) screen[1],0,0,new JLabel());
+	G2 = I2.createGraphics();
+    }
+ 
+    /**
+     * This method will draw a red line on both BufferedImage from the source point
+     * to the destination point.
+     * @param x1 is the x-coordinate of the source point.
+     * @param y1 is the y-coordinate of the source point.
+     * @param x2 is the x-coordinate of the destination point.
+     * @param y2 is the y-coordinate of the destination point. 
+     */
+    public void drawLines(int x1,int y1,int x2,int y2)
+    {
+	G2.setColor(Color.red);
+	G.setColor(Color.red);
+
+	G.setStroke(new BasicStroke(2.5f));
+	G2.setStroke(new BasicStroke(2.5f));
+
+	G2.drawLine(x1,y1,x2,y2);
+	G.drawLine(x1,y1,x2,y2);
+    }
+
+    
+    /**
+     * This method will draw a white line on all 3 BufferedImage from the source point
+     * to the destination point.
+     * @param x1 is the x-coordinate of the source point.
+     * @param y1 is the y-coordinate of the source point.
+     * @param x2 is the x-coordinate of the destination point.
+     * @param y2 is the y-coordinate of the destination point. 
+     */
+    public void eraseLines(int x1,int y1,int x2,int y2)
+    {
+	G3.setColor(Color.white);
+	G2.setColor(Color.white);
+	G.setColor(Color.white);
+
+	G.setStroke(new BasicStroke(25.5f));
+	G2.setStroke(new BasicStroke(25.5f));
+	G3.setStroke(new BasicStroke(25.5f));
+	
+	G3.drawLine(x1,y1,x2,y2);
+	G2.drawLine(x1,y1,x2,y2);
+	G.drawLine(x1,y1,x2,y2);
+    }
+
+    /**
+     * This method will get the first BufferedImage.
+     * This is the BufferedImage where it will already be drawn with the
+     * screen shot image and the lines.
+     * @return the first BufferedImage.
+     */
+    public BufferedImage getImage ()
+    {
+	return I;
+    }
+
+    /**
+     * This method will get the second BufferedImage.
+     * This is the BufferedImage where its base is transparent and it will
+     * already be drawn with the lines.
+     * @return the second BufferedImage.
+     */
+    public BufferedImage getImage2()
+    {
+	return I2;
+    }
+
+    /**
+     * This method will get the image without scribble lines only the white lines.
+     * @return the image without scribble lines.
+     */
+    public BufferedImage getScreenImage()
+    {
+	return screen[0];
+    }
+
+    /**
+     * This method will get the window out of the whole screen image.
+     * @param xcoord the xcoordinate of the window in the image.
+     * @param ycoord the ycoordinate of the window in the image.
+     * @param width  the width of the window in the image.
+     * @param height the height of the window in the image.
+     * @return the BufferedImage of the window only from the 3 buffered image.
+     */
+    public BufferedImage[] getWindowVersion(int xcoord,int ycoord,int width,int height)
+    {
+	BufferedImage[] window;
+	window = new BufferedImage[3];
+
+	Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+	
+	if (xcoord < 0 ) 
+	    xcoord = 0;
+	if (ycoord < 0)
+	    ycoord = 0;
+
+	if (xcoord >= imwidth)
+	    xcoord = (int)(imwidth - 1);
+	if (ycoord >= imheight)
+	    ycoord = (int)(imheight - 1);
+	if ((xcoord + width) > imwidth)
+	    width = (int)(imwidth - xcoord);
+	if ((ycoord + height) > imheight)
+	    height = (int)(imheight - ycoord);
+
+	if (width <= 0)
+	    width = 1;
+	if (height <= 0)
+	    height = 1;
+
+	window[0] = getClipVersion(screen[0],xcoord,ycoord,width,height);
+	
+	window[1] = getClipVersion(I,xcoord,ycoord,width,height);
+	
+	window[2] = getClipVersion(I2,xcoord,ycoord,width,height);
+	
+	return window;
+    }
+    
+    /**
+     * This method will return the subimage of the big buffered image.
+     * @param tag the original buffered image.
+     * @param x   the x location of subimage.
+     * @param y   the y location of subimage.
+     * @param w   the width of the subimage.
+     * @param h   the height of the subimage.
+     * @return    the subimage.
+     */
+    public BufferedImage getClipVersion (BufferedImage tag,int x, int y, int w, int h) 
+    {
+	BufferedImage sub;
+	sub = tag.getSubimage(x,y,w,h);
+	
+	return sub;
+    }
+
+    /**
+     * This method will return the screen image version of the give buffered image.
+     * @param sh the buffered image that we going to draw in the screen image version.
+     * @param xcoord   the x location of subimage.
+     * @param ycoord   the y location of subimage.
+     * @param w   the width of the subimage.
+     * @param h   the height of the subimage.
+     */
+    public void setScreenVersion(BufferedImage[] sh,int xcoord,int ycoord,int w, int h)
+    {
+	if (xcoord < 0 ) 
+	    xcoord = 0;
+	if (ycoord < 0)
+	    ycoord = 0;
+
+	if (xcoord >= imwidth)
+	    xcoord = (int) (imwidth - 1);
+	if (ycoord >= imheight)
+	    ycoord = (int) (imheight - 1);
+	if ((xcoord + w) > imwidth)
+	    w = (int) (imwidth - xcoord);
+	if ((ycoord + h) > imheight)
+	    h = (int) (imheight - ycoord);
+
+	if (w <= 0)
+	    w = 1;
+	if (h <= 0)
+	    h = 1;
+       
+	G3.drawImage(sh[0], xcoord,ycoord,new JLabel());
+
+        
+        G.drawImage(sh[1], xcoord,ycoord,new JLabel());
+	
+	
+	G2.drawImage(sh[2], xcoord,ycoord,new JLabel());
+    }
+}
+    
+
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/History.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/History.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/History.java	(revision 31635)
@@ -0,0 +1,284 @@
+package org.greenstone.gatherer.feedback;
+
+import java.util.*;
+import java.awt.image.*;
+import java.io.*;
+import javax.imageio.*;
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * This class is one record of the action user did to any of the component
+ * inside the window that the application open that is recorded by the 
+ * special listener class.
+ * An instance of this class will hold all the information about the action
+ * user did at that moment,all information about all the components inside the
+ * window where the action occured and all information about all the components
+ * inside all the windows that were opened when the action occured.
+ * @author Veronica Liesaputra
+ */
+public class History implements Serializable
+{
+    /**
+     * This is the date of when the action is done by the user.
+     */
+    private String date;
+    
+    /**
+     * This is the command that user did.
+     */
+    private String command;
+    
+    /**
+     * This is the title of the window where the action occured.
+     */
+    private String title;
+
+    /**
+     * This hold all information about all the components inside the
+     * window where the action occured and all information about all the components
+     * inside all the windows that were open when the action occured.
+     */
+    private ArrayList component;
+
+    /**
+     * This hold the image of the window where the action occured.
+     */
+    private String image = null;
+
+    /**
+     * This is the image file name, if then user decide to save the image to a jpeg file.
+     */
+    private String imageName = "";
+
+    /**
+     * This is the height of image of the window where the action occured. 
+     */
+    private String height;
+
+    /**
+     * This is the width of image of the window where the action occured.
+     */
+    private String width;
+
+    /**
+     * This constructor will be initializing the date,command and component value.
+     * (Precondition: (curr_date != null))
+     * @param curr_date is the date when the action occured.
+     * @param curr_command is the command the user did.
+     */
+    public History (Date curr_date,String curr_command)
+    {
+	date = curr_date.toString();
+	command = curr_command;
+	component = new ArrayList();
+    }
+
+    /**
+     * This method will be setting up the image attributes to the passed values.
+     * It will setting the image of the window where the action occured and its height 
+     * and width.It will also generate the supposed file name for this image.
+     * (Precondition: (imag != null) && (h != null) && (w != null))
+     * @param img the image of the window where the action occured.
+     * @param h   image's height.
+     * @param w   image's width.
+     */
+    public void setImage(String img,String h,String w)
+    {
+	image = img;
+	height = h;
+	width = w;
+	StringTokenizer st;
+	st = new StringTokenizer(date,": ");
+	while (st.hasMoreTokens()) 
+	    {
+		imageName += st.nextToken();
+	    }
+	imageName += ".jpg";
+    }
+
+    /**
+     * This method will add a new CompGroup instance that hold information and
+     * the status of a window that were open when the action occured.
+     * @param comp UserComponent instance that hold information about all
+     *             components inside the window.
+     * @param curr_status The current status of the window.
+     */
+    public void addComponent (UserComponent comp,String curr_status)
+    {
+	component.add(new CompGroup(comp,curr_status));
+    }
+
+    /**
+     * This method will get the image's height of the window where the action
+     * occured.
+     * @return image's height.
+     */
+    public String getHeight ()
+    {
+	return height;
+    }
+
+    /**
+     * This method will get the image's width of the window where the action
+     * occured.
+     * @return image's width.
+     */
+    public String getWidth ()
+    {
+	return width;
+    }
+
+    /**
+     * This method will set the title of the window where the action occured.
+     * @param name window's title.
+     */
+    public void setTitle (String name)
+    {
+	title = name;
+    }
+
+    /**
+     * This method will get the title of the window where the action occured.
+     * @return the window's title.
+     */
+    public String getTitle ()
+    {
+	return title;
+    }
+
+    /**
+     * This method will get the date when the action occured.
+     * @return the date when the action occured.
+     */
+    public String getDate()
+    {
+	return date;
+    }
+
+    /**
+     * This method will get the command that user did.
+     * @return the command user did.
+     */
+    public String getCommand()
+    {
+	return command;
+    }
+
+    /**
+     * This method will get the array of CompGroup instances that hold
+     * all the information and status of all the window that were open when 
+     * the action occured.
+     * @return the array list of the CompGroup instances.
+     */
+    public ArrayList getArray()
+    {
+	return component;
+    }
+
+    /**
+     * This method will get the image of the window where the action occured.
+     * @return image the window's image.
+     */
+    public String getImage()
+    {
+	return image;
+    }
+    
+    /**
+     * This method will get the supposed image's file name.
+     * @return image's file name.
+     */
+    public String getImageName()
+    {
+	return imageName;
+    }
+ 
+    /**
+     * This method will stored this instance to an xml file.
+     * It will only store the date,title,command,image and all
+     * image attributes of this instance to the xml file.
+     * @param sx is the xml file where we want to store.
+     */
+    public void sendXMLComm (SaveToXML sx)
+    {
+	sx.startContent(10);
+	
+	sx.startContent(7);
+	sx.saveContent(date);
+	sx.closeContent(7);
+	
+	sx.startContent(2);
+	sx.saveContent(title);
+	sx.closeContent(2);
+	
+	sx.startContent(8);
+	sx.saveContent(command);
+	sx.closeContent(8);
+
+	if (image != null)
+	    {
+		sx.startContent(6);
+		sx.saveImage(image,imageName,width,height);
+		sx.closeContent(6);
+	    }
+	
+	sx.closeContent(10);
+    }
+
+    /**
+     * This method will stored this instance to an xml file.
+     * It will store all the data members of this instance to
+     * the xml file.
+     * @param sx is the xml file where we want to store.
+     */
+    public void sendXML (SaveToXML sx)
+    {
+	sx.startContent(10);
+
+	sx.startContent(7);
+	sx.saveContent(date);
+	sx.closeContent(7);
+
+	sx.startContent(2);
+	sx.saveContent(title);
+	sx.closeContent(2);
+
+	sx.startContent(8);
+	sx.saveContent(command);
+	sx.closeContent(8);
+
+	if (image != null)
+	    {
+		sx.startContent(6);
+		sx.saveImage(image,imageName,width,height);
+		sx.closeContent(6);
+	    }
+	
+	int j;
+	for (j = 0 ; j < component.size() ; j++)
+	    {
+		sx.startContent(9);
+		CompGroup comp;
+		comp = (CompGroup) component.get(j);
+		
+		sx.startContent(8);
+		sx.saveContent(comp.getStatus());
+		sx.closeContent(8);
+
+		sx.startContent(0);
+		comp.getComponent().sendingXML(sx);
+		sx.closeContent(0);
+		
+		sx.closeContent(9);
+	    }
+	sx.closeContent(10);
+    }
+}
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Movie.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Movie.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Movie.java	(revision 31635)
@@ -0,0 +1,375 @@
+package org.greenstone.gatherer.feedback;
+
+import java.awt.image.*;
+import javax.swing.*;
+import java.awt.event.*;
+import java.awt.*;
+import javax.swing.event.*;
+import java.io.*;
+import java.util.ResourceBundle;
+import java.text.MessageFormat;
+import javax.swing.text.NumberFormatter;
+import java.text.NumberFormat;
+import java.beans.*;
+import java.util.ArrayList;
+
+/**
+ * This is the class to generate a animation type from a list of ImageIcon.
+ * It will display a JDialog window and then user can specify the frames
+ * per second rate that the user want to use to view the list of the ImageIcon.
+ * @author Veronica Liesaputra
+ */
+public class Movie extends JDialog
+    implements ActionListener,ChangeListener,PropertyChangeListener
+{
+    /**
+     * This variable will hold the resource of the words that is stored in Messages.properties file.
+     * The calling using messages.getString(someString) will caused someString to be translated
+     * into some other string that is hold in that file.usually it will caused it to be translated
+     * to the language that the user use or choose to have as stated in Locale.
+     */
+    private static ResourceBundle messages;
+
+    /**
+     * This is the label that we use to display the ImageIcon.
+     */
+    private JLabel picture;
+
+    /**
+     * This is a textField where user can set how many frames per second they want to view the
+     * ImageIcon.If user enter value that is bigger than the FPS_MAX then it will beep and user
+     * will need to enter new valid value or it will stay as the previous valid value.
+     * If user enter 0 then it will stop the animation.
+     */
+    private JFormattedTextField textField;
+
+    /**
+     * This is a slider where user can set how many frames per second they want to view the
+     * ImageIcon.If user slide to 0 then animation stop.
+     */
+    private JSlider framesPerSecond;
+    
+    /**
+     * This is the minimum frame per second value.
+     */
+    private int FPS_MIN = 0;
+
+    /**
+     * This is the maximum frame per second value. The value is the twice the amount of 
+     * ImageIcon in the list.
+     */
+    private int FPS_MAX;
+
+    /**
+     * This is the initial frame per second value to view the ImageIcon.
+     */
+    private int FPS_INIT = 1;
+
+    /**
+     * This is the ImageIcon index that are currently showing in the label.
+     */
+    private int frameNumber;
+
+    /**
+     * This is the total number of ImageIcon in the list.
+     */
+    private int NUM_FRAMES;
+
+    /**
+     * This hold the list of the ImageIcon that are to be displayed.
+     */
+    private ArrayList images;
+
+    /**
+     * This is telling how long is the delay for each frame to be refreshed.
+     */
+    private int delay;
+
+    /**
+     * This is the timer that generate the animation of the list of ImageIcon.
+     * The animation will be paused twice per cycle by restarting the timer.
+     * If the the prame per second chosen by the user is 0 then the timer
+     * will be stop.
+     */
+    private Timer timer;
+
+    /**
+     * This the flag to say when the animation should stop.
+     * If frozen = true then the animation should stop otherwise the animation
+     * should keep running.
+     */
+    private boolean frozen = false;
+
+    /**
+     * This will sho a modal-dialog and set up the value for the animation to run properly 
+     * according to the list of ImageIcon passed in.
+     * @param msg  hold the resource of the words that is stored in Messages.properties file.
+     * @param img  hold the list of ImageIcon to be displayed.
+     */
+    public Movie (ResourceBundle msg,ArrayList img)
+    {
+	super();
+
+	images = img;
+	NUM_FRAMES = img.size(); 
+	FPS_MAX = img.size() * 2;
+
+	messages = msg; 
+	setTitle(messages.getString("GenerateMovie"));
+	setModal(true);
+	setBackground(new Color (176,208,176));
+    }
+
+    /**
+     * This method will seeting up the content pane of the dialog window.
+     * It also setting up the timer and the delay.
+     * @return the content pane of the window.
+     */
+    public JPanel create_UI()
+    {
+	JPanel cont;
+	cont = new JPanel();
+	cont.setOpaque(true);
+	cont.setBackground(new Color (176,208,176));
+
+	JButton cls;
+	cls = new JButton(messages.getString("Close"));
+	cls.addActionListener(new ActionListener ()
+	    {
+		public void actionPerformed (ActionEvent e)
+		{
+		    dispose();
+		    if (timer != null)
+			timer.stop();
+		}
+	    });
+	cls.setDefaultCapable(true);
+	cls.setActionCommand(messages.getString("Close"));
+	cls.setBackground(new Color(176,208,176));
+
+
+	if (images.size() <= 0)
+	    {
+		JLabel empty_lbl;
+		empty_lbl = new JLabel ("No Image to be displayed   ");
+		cont.add(empty_lbl);
+		cont.add(cls);
+		return cont;
+	    }
+
+	cont.setLayout(new BoxLayout(cont, BoxLayout.PAGE_AXIS));
+
+	delay = 1000 / FPS_INIT;
+
+	JLabel sliderLabel;
+	sliderLabel = new JLabel(messages.getString("FramesPerSecond") + ": ", JLabel.CENTER);
+	sliderLabel.setBackground(new Color(176,208,176));
+        sliderLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
+
+        NumberFormat numberFormat;
+	numberFormat = NumberFormat.getIntegerInstance();
+        NumberFormatter formatter;
+	formatter = new NumberFormatter(numberFormat);
+        formatter.setMinimum(new Integer(FPS_MIN));
+        formatter.setMaximum(new Integer(FPS_MAX));
+
+        textField = new JFormattedTextField(formatter);
+	textField.setBackground(new Color(224,240,224));
+        textField.setValue(new Integer(FPS_INIT));
+        textField.setColumns(5);
+        textField.addPropertyChangeListener(this);
+
+        textField.getInputMap().put(KeyStroke.getKeyStroke(
+                                        KeyEvent.VK_ENTER, 0),
+                                        "check");
+        textField.getActionMap().put("check", new AbstractAction() 
+	    {
+		public void actionPerformed(ActionEvent e) 
+		{
+		    if (!textField.isEditValid()) 
+			{ 
+			    Toolkit.getDefaultToolkit().beep();
+			    textField.selectAll();
+			} 
+		    else
+			{ 
+			    try 
+				{                  
+				    textField.commitEdit();
+				} 
+			    catch (java.text.ParseException exc) { }
+			}
+		}
+	    });
+
+        framesPerSecond = new JSlider(JSlider.HORIZONTAL,
+                                      FPS_MIN, FPS_MAX, FPS_INIT);
+        framesPerSecond.addChangeListener(this);
+	framesPerSecond.setBackground(new Color(176,208,176));
+	framesPerSecond.setMajorTickSpacing(10);
+        framesPerSecond.setMinorTickSpacing(1);
+        framesPerSecond.setPaintTicks(true);
+        framesPerSecond.setPaintLabels(true);
+        framesPerSecond.setBorder(BorderFactory.createEmptyBorder(0,0,10,0));
+	
+        picture = new JLabel();
+	Dimension dim;
+	int iw,ih;
+	dim = Toolkit.getDefaultToolkit().getScreenSize();
+	iw = (int) (dim.width - 30);
+	ih = (int) (dim.height * 0.75);
+	picture.setPreferredSize(new Dimension(iw,ih));
+        picture.setHorizontalAlignment(JLabel.CENTER);
+        picture.setAlignmentX(Component.CENTER_ALIGNMENT);
+        picture.setBorder(BorderFactory.createCompoundBorder(
+                BorderFactory.createLoweredBevelBorder(),
+                BorderFactory.createEmptyBorder(10,10,10,10)));
+	picture.setBackground(new Color(176,208,176));
+        updatePicture(0);
+
+	
+        JPanel labelAndTextField;
+	labelAndTextField = new JPanel();
+        labelAndTextField.add(sliderLabel);
+        labelAndTextField.add(textField);
+	labelAndTextField.add(cls);
+	labelAndTextField.setBackground(new Color(176,208,176));
+
+	cont.add(picture);
+	cont.add(framesPerSecond);
+        cont.add(labelAndTextField);
+       
+        cont.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
+
+        timer = new Timer(delay, this);
+        timer.setInitialDelay(delay * 7); 
+        timer.setCoalesce(true);
+	timer.start();
+	
+	return cont;
+    }
+
+    /**
+     * This method will tell what should happen when the slider's arrow
+     * change its position.
+     * <br>If user adjusting the frame per second value using the slider
+     * then it will set the Textfield to have the value that slider point 
+     * to at the moment.If the value is 0 then it will stop the animation
+     * otherwise it will keep animate with the new frame per second value.
+     * @param e is the slider when the slider's arrow change its position.
+     */
+    public void stateChanged(ChangeEvent e) 
+    {
+        JSlider source;
+	source = (JSlider)e.getSource();
+        int fps;
+	fps = (int)source.getValue();
+        if (!source.getValueIsAdjusting()) 
+	    { 
+		textField.setValue(new Integer(fps));
+		if (fps == 0) 
+		    {
+			if (!frozen) 
+			    stopAnimation();
+		    } 
+		else 
+		    {
+			delay = 1000 / fps;
+			timer.setDelay(delay);
+			timer.setInitialDelay(delay * 10);
+			if (frozen) 
+			    startAnimation();
+		    }
+	    } 
+	else 
+	    { 
+		textField.setText(String.valueOf(fps));
+	    }
+    }
+   
+    /**
+     * This method will set the slider value when the user adjusting
+     * the frame per second value using the textfield.
+     * <br>If user adjusting the frame per second value using text field, the
+     * slider's arrow position will be changed into that value.
+     * @param e the text field
+     */
+    public void propertyChange(PropertyChangeEvent e) 
+    {
+        if ("value".equals(e.getPropertyName())) 
+	    {
+		Number value;
+		value = (Number)e.getNewValue();
+		if (framesPerSecond != null && value != null) 
+		    {
+			framesPerSecond.setValue(value.intValue());
+		    }
+	    }
+    }
+
+    /**
+     * This method will start the timer.
+     */
+    public void startAnimation() 
+    {
+        timer.start();
+        frozen = false;
+    }
+
+    /**
+     * This method will stop the timer.
+     */
+    public void stopAnimation() 
+    {
+        timer.stop();
+        frozen = true;
+    }
+
+    /**
+     * This method will tell what should happen when the timer is start.
+     * When the timer start it will increase the framenumber and will
+     * update the label with the new image icon.
+     * If the timer already display all the image icon in the list, it 
+     * will restart the timer and reanimate the image icon again.
+     * @param e fires when the timer start.
+     */
+    public void actionPerformed(ActionEvent e) 
+    {
+        if (frameNumber == (NUM_FRAMES - 1))
+            frameNumber = 0;
+        else 
+            frameNumber++;
+        
+        updatePicture(frameNumber);
+
+        if ((frameNumber == (NUM_FRAMES - 1)) ||
+	    (frameNumber==(NUM_FRAMES/2 - 1)))
+            timer.restart();
+    }
+
+    /**
+     * This will update the label with the new ImageIcon.
+     * If the ImageIcon is null then the label will display an error message
+     * instead of the picture.
+     * @param frameNum this is the index of the ImageIcon to be displayed. 
+     */
+    protected void updatePicture(int frameNum) 
+    {
+        if (images.get(frameNumber) != null) 
+	    {
+		picture.setIcon((ImageIcon) images.get(frameNumber));
+	    } 
+	else 
+	    {
+		picture.setText(messages.getString("image")+" #"
+				+ frameNumber + " " +
+				messages.getString("notfound"));
+	    }
+    }
+}
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/MyTextArea.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/MyTextArea.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/MyTextArea.java	(revision 31635)
@@ -0,0 +1,85 @@
+/** This class is not currently used and therefore will not be compiled by makegli. */
+
+package org.greenstone.gatherer.feedback;
+
+import javax.swing.*;
+import javax.swing.text.*;
+import java.util.Date;
+import java.awt.Color;
+
+/**
+ * This class will allow us to changed what should be inputted to the 
+ * text area as the user inputted the text to the text area.
+ * @author Veronica Liesaputra
+ */
+public class MyTextArea extends JTextArea 
+{
+    /**
+     * Creating an empty JTextArea.
+     */
+    public MyTextArea ()
+    {
+	super();
+    }
+
+    /**
+     * Creating a JTextArea already appended with cols.
+     * (Precondition: (cols != null))
+     * @param cols the text to be appended.
+     */
+    public MyTextArea(String cols) 
+    {
+	super(cols);
+    }
+ 
+    /**
+     * It will give the default model this text area should use.
+     * @return the default model this text area used.
+     */
+    protected Document createDefaultModel() 
+    {
+	return new UpperCaseDocument();
+    }
+ 
+    /**
+     * This class will be used as the default model of MyTextArea.
+     * Here we can control and modified the string that is going to be inserted to 
+     * the document.
+     */
+    static class UpperCaseDocument extends PlainDocument 
+    {
+	/**
+	 * This method will caused the document to be appended with the date followed by newline
+	 * everytime user trying to insert a new line to the document.
+	 * (Precondition : (offs >= 0) && (str != null))
+	 * @param offs is the offset placted to insert the string.
+	 * @param str is the string to be inserted.
+	 * @param a is the attribute set to be used.
+	 */
+	public void insertString(int offs, String str, AttributeSet a) 
+	    throws BadLocationException 
+	{
+	    if (str == null) 
+		{
+		    return;
+		}
+	    
+	    char[] upper;
+	    upper = str.toCharArray();
+
+	    if (str.endsWith("\n") == true)
+		{
+		    String str2;
+		    str2 = str + ((new Date()).toString()) + "\n\n>";
+		    upper = str2.toCharArray();
+		}
+	   
+	    super.insertString(offs, new String(upper), a);
+	}
+    }
+}
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ReportDetails.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ReportDetails.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ReportDetails.java	(revision 31635)
@@ -0,0 +1,1304 @@
+package org.greenstone.gatherer.feedback;
+
+import java.io.*;
+import java.awt.image.*;
+import javax.swing.*;
+import java.awt.*;
+import java.util.*;
+import javax.swing.event.MouseInputAdapter;
+import java.awt.event.*;
+import javax.swing.border.*;
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.text.MessageFormat;
+import javax.swing.tree.*;
+
+/**
+ * This class can give preview of all the history of actions user did or give preview of all the
+ * xml files that are ready to be send and it also can give preview of all the screen shot of
+ * all window taken when user did the actions.
+ * If user select Details button then they will view all xml files that are going to be send.
+ * If user select View History then they will see the history record that we took so far.
+ * @author Veronica Liesaputra
+ */
+public class ReportDetails extends WindowAdapter 
+{
+    /**
+     * This is the tree that will be used to hold the feedback form,the history stored inside the vector 
+     * at the moment, and hold the comment user did when they recreating the event.
+     * This tree's data partly will be supplied from whats inside vector. 
+     */
+    private JTree tree;
+    /**
+     * This is the root node for tree.
+     */
+    private DefaultMutableTreeNode rootNode;
+    /**
+     * This hold the parent node of the current node that is currently just being added to tree.
+     */
+    private DefaultMutableTreeNode parentNode;
+    /**
+     * This hold the current node that is currently just being added to tree.
+     */
+    private DefaultMutableTreeNode currNode;
+    /**
+     * This is the tree model for tree.
+     */
+    private DefaultTreeModel treeModel;
+
+    /**
+     * This is the tree that will be used to hold the commands (actions) done by the user inside the vector
+     * at the moment.This tree builds from partial data of tree and which data entered to this tree
+     * is signed by the value of comm_bool and comm_start. 
+     */
+    private JTree tree2;
+    /**
+     * This is the root node for tree2.
+     */
+    private DefaultMutableTreeNode rootNode2;
+    /**
+     * This is the parent node of the current node that is currently just being added to tree2.
+     */
+    private DefaultMutableTreeNode parentNode2;
+    /**
+     * This is the current node that is currently just being added to tree2.
+     */
+    private DefaultMutableTreeNode currNode2;
+    /**
+     * This is the tree model for tree2.
+     */
+    private DefaultTreeModel treeModel2;
+
+    /**
+     * This is the tree that will be used to hold the history of all the actions
+     * done by the user before they choose to do the Reporting Feedback sequence.
+     */
+    private JTree tree3;
+    /**
+     * This is the root node for tree3.
+     */
+    private DefaultMutableTreeNode rootNode3;
+    /**
+     * This is the parent node for the current node that is currently just being added to tree3.
+     */
+    private DefaultMutableTreeNode parentNode3;
+    /**
+     * This is the current node that is currently just being added to tree3.
+     */
+    private DefaultMutableTreeNode currNode3;
+    /**
+     * This is the tree model for tree3.
+     */
+    private DefaultTreeModel treeModel3;
+
+    /**
+     * This is the tree that will be used to hold the commands (actions) done by the user 
+     * before they choose to do the Reporting Feedback sequence.This tree builds from partial data of tree3 
+     * and which data entered to this tree2 is signed by the value of comm_bool and comm_start. 
+     */
+    private JTree tree4;
+    /**
+     * This is the root node for tree4.
+     */
+    private DefaultMutableTreeNode rootNode4;
+    /**
+     * This is the parent node for the current node that is currently just being added to tree4.
+     */
+    private DefaultMutableTreeNode parentNode4;
+    /**
+     * This is the current node that is currently just being added to tree4.
+     */
+    private DefaultMutableTreeNode currNode4;
+    /**
+     * This is the tree model for tree4.
+     */
+    private DefaultTreeModel treeModel4;
+
+    /**
+     * This is the panel to display tree4.
+     */
+    private JPanel pane4;
+    /**
+     * This is the scroll pane for displaying tree3, so it is scrollable.
+     */
+    private JScrollPane scroll3;
+    /**
+     * This is the scroll pane for displaying tree4, so it is scrollable.
+     */
+    private JScrollPane scroll4;
+
+    /**
+     * This is the panel to display tree2.
+     */
+    private JPanel pane2;
+    /**
+     * This is the scroll pane for displaying tree2, so it is scrollable.
+     */
+    private JScrollPane scroll2;
+    /**
+     * This is the scroll pane for displaying tree, so it is scrollable.
+     */
+    private JScrollPane scroll;
+   
+    /**
+     * This is all the information taken from the Feedback form and information that
+     * was taken from the system.
+     */
+    private String[] err_details;
+    
+    /**
+     * This is the vector that hold all information about the history of all
+     * the actions user did.
+     * If rec = true, it means it hold the history of all the actions and information
+     * of all the windows were open when user did the actions during reporting feedback
+     * sequence.
+     * If rec = false, it means it hold the history of all the actions and information
+     * of all the windows were open when user did the actions before and after
+     * reporting feedback sequence.
+     */
+    private Vector vector;
+    
+    /**
+     * This is a variable to know which tag xml data that wants to be added to the JTree. 
+     */
+    private int count;
+    
+    /**
+     * This is a flag variable to sign whether or not the next data is to be added to
+     * the command JTree. 
+     */
+    private boolean comm_bool = false;
+    /**
+     * This is a flag variable to sign whether or not the next history is to be added to
+     * the command JTree.
+     */
+    private boolean comm_start = false;
+    /**
+     * This is a flag variable to sign whether or not at the moment we building a new tree
+     * or build from the existing tree.
+     */
+    private boolean new_tree = false;
+    
+    /**
+     * This is a flag variable to sign which type of preview that user wants at the moment.
+     * If its true, then its means user will view all the 6 xml files that will be send.
+     * If its false, then its means user only wants to view the history and command user just did
+     * before and after the reporting feedback sequence.
+     */
+    private boolean rec = false;
+   
+    /**
+     * This is array list of all the image icons that are inside all the command JTree.
+     */
+    private ArrayList movie;
+
+    /**
+     * This variable will hold the resource of the words that is stored in Messages.properties file.
+     * The calling using messages.getString(someString) will caused someString to be translated
+     * into some other string that is hold in that file.usually it will caused it to be translated
+     * to the language that the user use or choose to have as stated in Locale.
+     */
+    private static ResourceBundle messages;
+
+    private String[] img;
+   
+    /**
+     * This constructor will be seeting up the data member to its appropriate value.
+     * (Precondition : (msg != null) && (bool != null))
+     * @param stack this is all vector to be supplied to tree.
+     * @param err_array holding all the information taken when user doing the FeedbackInterface form.
+     * @param imgFile holding all the information inside all the panels in the FeedbackInterface form.
+     * @param msg hold the resource of the words that is stored in Messages.properties file.
+     * @param bool hold the falg value of rec.
+     */
+    public ReportDetails(Vector stack,String[] err_array,String[] imgFile,ResourceBundle msg,boolean bool)
+    {
+	img = imgFile;
+	vector = stack;
+	err_details = err_array;
+	messages = msg;
+	rec = bool;
+	movie = new ArrayList();
+	makeNewWindow();
+    }
+
+    /**
+     * This method will make a modal-dialog window that will display all the preview.
+     */
+    public void makeNewWindow()
+    {
+	JDialog frame;
+	frame = new View (this);
+	frame.setModal(true);
+	frame.setVisible(true);
+    }
+
+    /**
+     * This class is the dialog window that will give the preview.
+     */
+    class View extends JDialog implements ActionListener
+    {
+	/**
+	 * This variable is holding the current window that display the preview.
+	 */
+	private JDialog frame;
+	/**
+	 * This variable holding the owner of this window.
+	 */
+	private ReportDetails framework = null;
+
+	/**
+	 * This constructor will make the dialog window for previewing and setting up the
+	 * content pane of this window.
+	 * @param controller is the owner of this window.
+	 */
+	public View (ReportDetails controller)
+	{
+	    super();
+	    framework = controller;
+	    frame = this;
+
+	    setTitle(messages.getString("ViewReport"));  
+	    setBackground(new Color(176,208,176));
+	    setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+	    setDefaultLookAndFeelDecorated(true);
+	 
+	    JPanel cont;
+	    cont = new JPanel();
+	    cont.setLayout(new BoxLayout(cont,BoxLayout.PAGE_AXIS));
+	    cont.setBackground(new Color(176,208,176));
+	
+	    JTabbedPane tab;
+	    tab = new JTabbedPane();
+	    tab.setBackground(new Color(176,208,176));
+
+	    new_tree = false;
+
+	    if (err_details != null)
+		{
+		    tab.addTab(messages.getString("ViewFeedbackForm"), 
+			       createFeedback_UI(err_details,img));
+		}
+	    
+	    if (rec == true)
+		{
+		    tab.addTab(messages.getString("ViewFeedbackHistoryState"),
+			       createFeedbackHistory_UI(vector));
+		    tab.addTab(messages.getString("ViewFeedbackCommandState"),pane2);
+		    /*tab.addTab(messages.getString("ViewHistoryState"),
+		      createHistory_UI());*/
+		    createHistory_UI();
+		    tab.addTab(messages.getString("ViewCommandState"),pane4);
+		}
+	    else
+		{
+		    /*tab.addTab(messages.getString("ViewHistoryState"),
+		      createFeedbackHistory_UI(vector));*/
+		    createFeedbackHistory_UI(vector);
+		    tab.addTab(messages.getString("ViewCommandState"),pane2);
+		}
+
+	    cont.add(tab);
+
+	    JLabel lbl;
+	    lbl = new JLabel("   ");
+	    lbl.setBackground(new Color(176,208,176));
+
+	    cont.add(lbl);
+
+	    JPanel pane3;
+	    pane3 = new JPanel();
+	    pane3.setBackground(new Color(176,208,176));
+	    JButton button;
+	    button = new JButton(messages.getString("Close"));
+	    button.setActionCommand(messages.getString("Close"));
+	    button.addActionListener(this);
+	    button.setDefaultCapable(true);
+	    button.setToolTipText(messages.getString("ClosePreview"));
+	    button.setBackground(new Color(176,208,176));
+	   
+	    /*JButton button2;
+	    button2 = new JButton(messages.getString("ViewMovie"));
+	    button2.setActionCommand(messages.getString("ViewMovie"));
+	    button2.addActionListener(new ActionListener()
+		{
+		    public void actionPerformed (ActionEvent e)
+		    {
+			JDialog.setDefaultLookAndFeelDecorated(true);
+			JDialog frame;
+			frame = new Movie(messages,movie);
+
+			frame.setContentPane(((Movie)frame).create_UI());
+			frame.pack();
+			frame.setVisible(true);
+		    }
+		});
+	    button2.setToolTipText(messages.getString("ViewMovieText"));
+	    button2.setBackground(new Color(176,208,176));
+	    pane3.add(button2);*/
+	    pane3.add(button);
+	    pane3.setBorder(new EmptyBorder(new Insets(0,10,10,10)));
+
+	    cont.add(pane3);
+
+	    cont.setBorder(new EmptyBorder(new Insets(5,5,5,5)));
+	    getContentPane().add(cont);
+
+	    tab.setAlignmentX(Component.LEFT_ALIGNMENT);
+	    lbl.setAlignmentX(Component.LEFT_ALIGNMENT);
+	    pane3.setAlignmentX(Component.LEFT_ALIGNMENT);
+	    
+	    addWindowListener(framework);
+	    Toolkit kit;
+	    kit = Toolkit.getDefaultToolkit();
+	    Dimension screenSize;
+	    screenSize = kit.getScreenSize();
+	    int screenHeight;
+	    screenHeight = screenSize.height;
+	    int screenWidth;
+	    screenWidth = screenSize.width;
+	    Dimension size;
+	    size = this.getSize();
+	    setLocation((screenWidth -size.width)/ 4, 
+			(screenHeight - size.height)/ 4);
+
+	    pack();
+	    setResizable(false);
+	}
+	
+	/**
+	 * This method will get the vector that is stored inside temp_feedbackhist.log
+	 * if rec is true, and will get the vector that is stored inside temp_history.log
+	 * if rec is false. If file is not exist then it will return null. 
+	 * @return vector that is stored in the file.
+	 */
+	public Vector getPrev_log()
+	{
+	    Vector stack = null;
+	    try
+		{
+		    File f;
+		    if (rec == true)
+			{
+			    f = new File("temp_feedbackhist.log");
+			}
+		    else
+			{
+			    f = new File("temp_history.log");
+			}
+
+		    if (f.exists() == true)
+		    {
+			FileInputStream fis = new FileInputStream(f);
+			ObjectInputStream ois = new ObjectInputStream(fis);
+			stack = (Vector) ois.readObject();
+			ois.close();
+			return stack;
+		    }
+		    else
+			stack = null; 
+		}
+	    catch (IOException exp) {exp.printStackTrace();}
+	    catch (ClassNotFoundException exp2) 
+		{
+		    System.out.println("class exp");}
+	    
+	    return stack;
+	}
+
+	/**
+	 * This method will update the graphics of this window and it will
+	 * set variable finish in Conformation to true.
+	 * @param g is the graphics for this window.
+	 */
+	public void paint (Graphics g)
+	{
+	    super.paint(g); 
+	    FeedbackInterface.setFinish(true);
+	}
+
+	/**
+	 * This method will close the preview window if user select close button.
+	 * @param e action event user did.
+	 */
+	public void actionPerformed (ActionEvent e)
+	{   
+	    if(messages.getString("Close").equals(e.getActionCommand()))
+		{ 
+		    dispose();
+		}
+	}
+
+	/**
+	 * This method will add a record to history tree and the command tree.
+	 * @param log is the record to be added.
+	 */
+	public void getHist(History log)
+	{
+	    comm_bool = true;
+	    startContent(10);
+
+	    startContent(7);
+	    saveContent(log.getDate());
+	    closeContent(7);
+	    
+	    startContent(2);
+	    saveContent(log.getTitle());
+	    closeContent(2);
+	    
+	    startContent(8);
+	    saveContent(log.getCommand());
+	    closeContent(8);
+	  
+	    String image;
+	    image = log.getImage();
+
+	    if (image != null)
+		{
+		    byte[] img;
+		    img = Base64.decode(image);
+		    
+		    String width,height;
+		    width = log.getWidth();
+		    height = log.getHeight();
+		    
+		    int w,h;
+		    w = Integer.parseInt(width);
+		    h = Integer.parseInt(height);
+
+		    ImageIcon icon,small_icon;
+		    small_icon = new ImageIcon(img);
+		    icon = new ImageIcon
+			(small_icon.getImage().getScaledInstance(w,h,Image.SCALE_SMOOTH));
+
+		    movie.add(0,icon);
+
+		    startContent(6);
+		    saveImage(log.getImageName(),width,height);
+		    closeContent(6);
+		}
+	    comm_bool = false;
+	    
+	    ArrayList component;
+	    component = log.getArray();
+	    int j;
+	    for (j = 0 ; j < component.size() ; j++)
+		{
+		    startContent(9);
+		    CompGroup comp;
+		    comp = (CompGroup) component.get(j);
+		    
+		    startContent(8);
+		    saveContent(comp.getStatus());
+		    closeContent(8);
+		    
+		    startContent(0);
+		    UserComponent cmp;
+		    cmp = comp.getComponent();
+		    showReport(cmp);
+		    closeContent(0);
+		    
+		    closeContent(9);
+		}
+	    
+	    comm_bool = true;
+	    closeContent(10);
+	    comm_bool = false;
+	}
+    
+	/**
+	 * This method will make the tree that contains the history and the commands of all the actions and window 
+	 * open when user did the action that stored in vector.
+	 * @param vector the vector that contains the information need to be display in tree.
+	 * @return       the panel that display tree and tree2.
+	 */
+	public JPanel createFeedbackHistory_UI(Vector vector)
+	{
+	    scroll = null;
+	    int k;
+
+	    tree = null;
+	    rootNode = null;
+	    currNode = null;
+	    parentNode =  null; 
+	    treeModel = null;
+
+	    tree2 = null;
+	    rootNode2 = null;
+	    currNode2 = null;
+	    parentNode2 =  null; 
+	    treeModel2 = null;
+	    
+	    comm_bool = true;
+	    comm_start = true;
+	    new_tree = false;
+
+	    startElement(messages.getString("HISTORY"));
+ 
+	    for (k = 0 ; k < vector.size() ; k++)
+		{
+		    History log;
+		    log = (History) vector.get(k);
+		    getHist(log);
+		}
+
+	    Vector stack;
+	    stack = getPrev_log();
+	    if (stack != null)
+		{
+		    for (k = 0 ; k < stack.size() ; k++)
+			{
+			    History log;
+			    log = (History) stack.get(k);
+			    getHist(log);
+			}
+		    stack.removeAllElements();
+		    System.gc();
+		    stack = null;
+		}
+
+	    comm_bool = true;
+	    endElement(messages.getString("HISTORY"));
+	    comm_bool = false;
+	    
+	    comm_start = false;
+	
+	    pane2 = new JPanel();
+	    pane2.add(scroll2);
+	    pane2.setBackground(new Color(176,208,176));
+	    pane2.setBorder(new EmptyBorder(new Insets(10,10,10,10)));
+
+	    JPanel pane;
+	    pane = new JPanel();
+	    pane.add(scroll);
+	    pane.setBackground(new Color(176,208,176));
+	    pane.setBorder(new EmptyBorder(new Insets(10,10,10,10)));
+	    
+	    return pane;
+	}
+
+	/**
+	 * This method will make the tree that contains the history and the commands of all the actions and window 
+	 * open when user did the action before user choose the reporting feedback sequence.
+	 * @return the panel contains the preview of this history and commands before user choose to start
+	 *         the reporting feedback sequence.
+	 */
+	public JPanel createHistory_UI()
+	{
+	    scroll3 = null;
+	    int k;
+
+	    tree3 = null;
+	    rootNode3 = null;
+	    currNode3 = null;
+	    parentNode3 =  null; 
+	    treeModel3 = null;
+
+	    tree4 = null;
+	    rootNode4 = null;
+	    currNode4 = null;
+	    parentNode4 =  null; 
+	    treeModel4 = null;
+	    
+	    comm_bool = true;
+	    comm_start = true;
+	    new_tree = true;
+
+	    startElement(messages.getString("HISTORY"));
+ 
+	    try
+		{
+		    File f = new File("history.log");
+		    FileInputStream fis = new FileInputStream(f);
+		    ObjectInputStream ois = new ObjectInputStream(fis);
+		    
+		    Vector stack;
+		    stack = (Vector) ois.readObject();
+
+		    if (stack != null)
+			{
+			    for (k = 0 ; k < stack.size() ; k++)
+				{
+				    History log;
+				    log = (History) stack.get(k);
+				    getHist(log);
+				}
+			    stack.removeAllElements();
+			    System.gc();
+			    stack = null;
+			}
+		    
+		    stack = (Vector) ois.readObject();
+		    
+		    if (stack != null)
+			{
+			    for (k = 0 ; k < stack.size() ; k++)
+				{
+				    History log;
+				    log = (History) stack.get(k);
+				    getHist(log);
+				}
+			    stack.removeAllElements();
+			    System.gc();
+			    stack = null;
+			}
+		
+		    ois.close();
+		}
+	    catch (IOException exp) {exp.printStackTrace();}
+	    catch (ClassNotFoundException exp2) 
+		{
+		    System.out.println("class exp");}
+	    
+	    comm_bool = true;
+	    endElement(messages.getString("HISTORY"));
+	    comm_bool = false;
+	    
+	    comm_start = false;
+	
+	    pane4 = new JPanel();
+	    pane4.add(scroll4);
+	    pane4.setBackground(new Color(176,208,176));
+	    pane4.setBorder(new EmptyBorder(new Insets(10,10,10,10)));
+
+	    JPanel pane;
+	    pane = new JPanel();
+	    pane.add(scroll3);
+	    pane.setBackground(new Color(176,208,176));
+	    pane.setBorder(new EmptyBorder(new Insets(10,10,10,10)));
+	    
+	    return pane;
+	}
+
+	/**
+	 * This method display all the information taken when user did the Feedback form.
+	 * @param err_details is the information taken when user did the Feedback form.
+	 * @param img         is the information from all the panels inside the Feedback form.
+	 * @return            panel displaying the information.
+	 */
+	public JPanel createFeedback_UI(String[] err_details,String[] img)
+	{
+	    scroll = null;
+	 
+	    tree = null;
+	    rootNode = null;
+	    currNode = null;
+	    parentNode =  null; 
+	    treeModel = null;
+
+	    tree2 = null;
+	    rootNode2 = null;
+	    currNode2 = null;
+	    parentNode2 =  null; 
+	    treeModel2 = null;
+
+	    startElement(messages.getString("BUGS"));
+	    
+	    startElement(messages.getString("IDCode"));
+	    characters(err_details[0].toCharArray(),0,err_details[0].length());
+	    endElement(messages.getString("IDCode"));
+	
+	    startElement(messages.getString("LastViewedWindow"));
+	    characters(err_details[1].toCharArray(),0,err_details[1].length());
+	    endElement(messages.getString("LastViewedWindow"));
+	    
+	    startElement(messages.getString("Optionals"));
+	   
+	    startElement(messages.getString("Type"));
+	    characters(err_details[3].toCharArray(),0,err_details[3].length());
+	    endElement(messages.getString("Type"));
+		
+	    startElement(messages.getString("Urgency"));
+	    characters(err_details[4].toCharArray(),0,err_details[4].length());
+	    endElement(messages.getString("Urgency"));
+	    
+	    endElement(messages.getString("Optionals"));
+	
+	    startElement(messages.getString("User"));
+	    
+	    startElement(messages.getString("Name"));
+	    characters(err_details[5].toCharArray(),0,err_details[5].length());
+	    endElement(messages.getString("Name"));
+	    
+	    startElement(messages.getString("HomeDirectory"));
+	    characters(err_details[6].toCharArray(),0,err_details[6].length());
+	    endElement(messages.getString("HomeDirectory"));
+	    
+	    startElement(messages.getString("WorkingDirectory"));
+	    characters(err_details[7].toCharArray(),0,err_details[7].length());
+	    endElement(messages.getString("WorkingDirectory"));
+	    
+	    startElement(messages.getString("SMTP"));
+	    characters(err_details[19].toCharArray(),0,err_details[19].length());
+	    endElement(messages.getString("SMTP"));
+	    
+	    startElement(messages.getString("Email"));
+	    characters(err_details[20].toCharArray(),0,err_details[20].length());
+	    endElement(messages.getString("Email"));
+
+	    endElement(messages.getString("User"));
+	    
+	    startElement(messages.getString("OpenFormTime"));
+	    characters(err_details[8].toCharArray(),0,err_details[8].length());
+	    endElement(messages.getString("OpenFormTime"));
+
+	    startElement(messages.getString("SendFormTime"));
+	    characters(err_details[9].toCharArray(),0,err_details[9].length());
+	    endElement(messages.getString("SendFormTime"));
+	    
+	    startElement(messages.getString("OperatingSystem"));
+	    characters(err_details[10].toCharArray(),0,err_details[10].length());
+	    endElement(messages.getString("OperatingSystem"));
+
+	    startElement(messages.getString("Java"));
+	    
+	    startElement(messages.getString("JavaInformation"));
+	    characters(err_details[11].toCharArray(),0,err_details[11].length());
+	    endElement(messages.getString("JavaInformation"));
+	    
+	    startElement(messages.getString("TotalMemory"));
+	    characters(err_details[21].toCharArray(),0,err_details[21].length());
+	    endElement(messages.getString("TotalMemory"));
+
+	    startElement(messages.getString("MaxMemory"));
+	    characters(err_details[22].toCharArray(),0,err_details[22].length());
+	    endElement(messages.getString("MaxMemory"));
+
+	    startElement(messages.getString("FreeMemory"));
+	    characters(err_details[23].toCharArray(),0,err_details[23].length());
+	    endElement(messages.getString("FreeMemory"));
+
+	    startElement(messages.getString("DefaultLocale"));
+	    characters(err_details[12].toCharArray(),0,err_details[12].length());
+	    endElement(messages.getString("DefaultLocale"));
+	
+
+	    endElement(messages.getString("Java"));
+	    
+	    startElement(messages.getString("Browser"));
+	    characters(err_details[13].toCharArray(),0,err_details[13].length());
+	    endElement(messages.getString("Browser"));
+	
+	    startElement(messages.getString("LocalHostName"));
+	    characters(err_details[14].toCharArray(),0,err_details[14].length());
+	    endElement(messages.getString("LocalHostName"));
+	
+	    startElement(messages.getString("LocalHostAddress"));
+	    characters(err_details[15].toCharArray(),0,err_details[15].length());
+	    endElement(messages.getString("LocalHostAddress"));
+	    
+	    startElement(messages.getString("ScreenResolution"));
+	    characters(err_details[16].toCharArray(),0,err_details[16].length());
+	    endElement(messages.getString("ScreenResolution"));
+
+	    if (img!=null)
+		{
+		    startElement("Sequences");
+		    int i;
+		    for ( i = 0 ; i < img.length ; i= i + 10)
+			{
+			    startElement("Sequence");
+			    
+			    startElement("Title");
+			    characters(img[i].toCharArray(),0,img[i].length());
+			    endElement("Title");
+			    
+			    startElement("Details");
+			    characters(img[i+9].toCharArray(),0,img[i+9].length());
+			    endElement("Details");
+			    
+			    startElement("ImageDescription");
+			    
+			    startElement("ScreenShot");
+			    startElement("Image");
+			    saveImage(img[i+2],img[i+7],img[i+8]);
+			    endElement("Image");
+			    endElement("ScreenShot");
+			    
+			    startElement("ErrorLineAndScreenShot");
+			    startElement("Image");
+			    saveImage(img[i+4],img[i+7],img[i+8]);
+			    endElement("Image");
+			    endElement("ErrorLineAndScreenShot");
+			    
+			    startElement("ErrorLineForScreenShot");
+			    startElement("Image");
+			    saveImage(img[i+6],img[i+7],img[i+8]); 
+			    endElement("Image");
+			    endElement("ErrorLineForScreenShot");
+			  
+			    endElement("ImageDescription");
+			    
+			    endElement("Sequence");
+			}
+		    endElement("Sequences");
+		}
+
+	    endElement(messages.getString("BUGS"));
+	    
+	    JPanel pane;
+	    pane = new JPanel();
+	    pane.add(scroll);
+	    pane.setBackground(new Color(176,208,176));
+	    pane.setBorder(new EmptyBorder(new Insets(10,10,10,10)));
+
+	    return pane;
+	}
+	
+	/**
+	 * This will add the UserComponent to the tree.
+	 * @param sx is the UserComponent to be added. 
+	 */
+	public void showReport (UserComponent sx)
+	{
+	    String type;
+	    type = sx.getType();
+	    String title;
+	    title = sx.getTitle();
+	    ArrayList content;
+	    content = sx.getContent();
+	    String selected;
+	    selected = sx.getSelected();
+	    String visible;
+	    visible = sx.getVisible();
+	    String image;
+	    image = sx.getImage();
+	    String img;
+	    img = sx.getImageFileName();
+	    String width;
+	    width = sx.getWidth();
+	    String height;
+	    height = sx.getHeight();
+	    String tooltip;
+	    tooltip = sx.getToolTip();
+	    
+	    if (type != null)
+		{
+		    startContent(1);
+		    saveContent(type);
+		    closeContent(1);
+		}
+	    
+	    if (title != null)
+		{
+		    startContent(2);
+		    saveContent(title);
+		    closeContent(2);
+		}
+	    
+	    if (content != null)
+		{
+		    startContent(3);
+		    int j;
+		    for ( j = 0 ; j < content.size() ; j++)
+			{
+			    if (content.get(j) instanceof String)
+				saveContent((String) content.get(j));
+			    else if (content.get(j) instanceof UserComponent)
+				{
+				    startContent(0);
+				    UserComponent cmp;
+				    cmp = (UserComponent) content.get(j);
+				    showReport(cmp);
+				    closeContent(0);
+				}
+			}
+		    closeContent(3);
+		}
+	    
+	    if (selected != null)
+		{
+		    startContent(4);
+		    saveContent(selected);
+		    closeContent(4);
+		}
+	    
+	    if (visible != null)
+		{
+		    startContent(5);
+		    saveContent(visible);
+		    closeContent(5);
+		}
+	
+	    if (img != null)
+		{
+		    startContent(6);
+		    saveImage(img,width,height);
+		    closeContent(6);
+		}
+	    else
+		{
+		    if (image != null)
+			{ 
+			    startContent(11);
+			    saveContent(image);
+			    closeContent(11);
+			} 
+		}
+
+	    if (tooltip != null)
+		{
+		    startContent(12);
+		    saveContent(tooltip);
+		    closeContent(12);
+		}
+	}
+	
+	/**
+	 * This method will give what type of information this node hold according the number
+	 * @param num the number that describe what type of information it hold.
+	 */
+	public void startContent (int num)
+	{
+	    switch(num)
+		{
+		case 0: startElement(messages.getString("COMPONENT"));
+		    break;
+		case 1: startElement(messages.getString("Type"));
+		    break;
+		case 2: startElement(messages.getString("Title"));
+		    break;
+		case 3: startElement(messages.getString("Content"));
+		    break;
+		case 4: startElement(messages.getString("Selected"));
+		    break;
+		case 5: startElement(messages.getString("Visible"));  
+		    break;
+		case 6: startElement(messages.getString("Image"));
+		    break;
+		case 7: startElement(messages.getString("Date"));
+		    break;
+		case 8: startElement(messages.getString("Command"));
+		    break;
+		case 9: startElement(messages.getString("COMPONENTS"));
+		    break;
+		case 10:startElement(messages.getString("LOG"));
+		    break;
+		case 11:startElement(messages.getString("Icon"));
+		    break;
+		case 12:startElement(messages.getString("ToolTipText"));
+		    break;
+		case 13:startElement(messages.getString("Comment"));
+		    break;
+		}
+	}
+	
+	/**
+	 * This will add the text content to the current node.
+	 * @param text is the text content of the current node.
+	 */
+	public void saveContent (String text)
+	{
+	    if (text == null)
+		text = "\t";
+	    characters(text.toCharArray(),0,text.length());
+	}
+	
+	/**
+	 * This will say that what type of information that has been finished added
+	 * accroding its number.
+	 * @param num  the number that describe what type of information it hold.
+	 */
+	public void closeContent (int num)
+	{
+	    switch(num)
+		{
+		case 0: endElement(messages.getString("COMPONENT"));
+		    break;
+		case 1: endElement(messages.getString("Type"));
+		    break;
+		case 2: endElement(messages.getString("Title"));
+		    break;
+		case 3: endElement(messages.getString("Content"));
+		    break;
+		case 4: endElement(messages.getString("Selected"));
+		    break;
+		case 5: endElement(messages.getString("Visible"));
+		    break;
+		case 6: endElement(messages.getString("Image"));
+		    break;
+		case 7: endElement(messages.getString("Date"));
+		    break;
+		case 8: endElement(messages.getString("Command"));
+		    break;
+		case 9: endElement(messages.getString("COMPONENTS"));
+		    break;
+		case 10:endElement(messages.getString("LOG"));
+		    break;
+		case 11:endElement(messages.getString("Icon"));
+		    break;
+		case 12:endElement(messages.getString("ToolTipText"));
+		    break;
+		case 13:endElement(messages.getString("Comment"));
+		    break;
+		}
+	}
+	
+	/**
+	 * This method will add the this string to the appropriate place in the tree.
+	 * @param type its the type to be added to the tree.
+	 */
+	public void startElement (String type)
+	{
+	   
+	    if ((comm_bool == true)&&(comm_start == true))
+		{
+		    if (new_tree == true)
+			{
+			    if (type == messages.getString("HISTORY"))
+				{
+				    rootNode4 = new DefaultMutableTreeNode(messages.getString("HISTORYCOMMANDS"));
+				    treeModel4 = new DefaultTreeModel(rootNode4);
+				    
+				    parentNode4 = rootNode4;
+				    tree4 = new JTree(treeModel4);
+				    tree4.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+				    tree4.setBackground(new Color(224,240,224));
+				    tree4.setEditable(false);
+				    tree4.setShowsRootHandles(true);
+				}  
+			    else
+				{
+				    currNode4 = new DefaultMutableTreeNode(type + " : ");
+				    treeModel4.insertNodeInto(currNode4,parentNode4,parentNode4.getChildCount());
+				    parentNode4 = currNode4;
+				} 
+			}
+		    else
+			{
+			    if (type == messages.getString("HISTORY"))
+				{
+				    rootNode2 = new DefaultMutableTreeNode(messages.getString("HISTORYCOMMANDS"));
+				    treeModel2 = new DefaultTreeModel(rootNode2);
+				    
+				    parentNode2 = rootNode2;
+				    tree2 = new JTree(treeModel2);
+				    tree2.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+				    tree2.setBackground(new Color(224,240,224));
+				    tree2.setEditable(false);
+				    tree2.setShowsRootHandles(true);
+				}  
+			    else
+				{
+				    currNode2 = new DefaultMutableTreeNode(type + " : ");
+				    treeModel2.insertNodeInto(currNode2,parentNode2,parentNode2.getChildCount());
+				    parentNode2 = currNode2;
+				} 
+			}
+		}
+		
+	    if ((type == messages.getString("COMMENTS"))||(type == messages.getString("BUGS"))||
+		(type == messages.getString("HISTORY")))
+		{
+		    if (new_tree == true)
+			{
+			    rootNode3 = new DefaultMutableTreeNode(type);
+			    treeModel3 = new DefaultTreeModel(rootNode3);
+			    
+			    parentNode3 = rootNode3;
+			    tree3 = new JTree(treeModel3);
+			    tree3.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+			    
+			    tree3.setBackground(new Color(224,240,224));
+			    tree3.setEditable(false);
+			    tree3.setShowsRootHandles(true);
+			}
+		    else
+			{
+			    rootNode = new DefaultMutableTreeNode(type);
+			    treeModel = new DefaultTreeModel(rootNode);
+			    
+			    parentNode = rootNode;
+			    tree = new JTree(treeModel);
+			    tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+			    
+			    tree.setBackground(new Color(224,240,224));
+			    tree.setEditable(false);
+			    tree.setShowsRootHandles(true);
+			}
+		}
+	    else
+		 {
+		     if (new_tree == true)
+			 {
+			     currNode3 = new DefaultMutableTreeNode(type + " : ");
+			     treeModel3.insertNodeInto(currNode3,parentNode3,parentNode3.getChildCount());
+			     parentNode3 = currNode3;
+			 }
+		     else
+			 {
+			     currNode = new DefaultMutableTreeNode(type + " : ");
+			     treeModel.insertNodeInto(currNode,parentNode,parentNode.getChildCount());
+			     parentNode = currNode;
+			 }
+		 }
+	}
+	
+	/**
+	 * This method is to mark that we finished adding the type to the tree.
+	 * @param type type that finished added to the tree.
+	 */
+	public void endElement (String type)
+	{
+	    if ((type == messages.getString("HISTORY"))||(type == messages.getString("BUGS"))||
+		(type == messages.getString("COMMENTS")))
+		{
+		    if (new_tree == true)
+			{
+			    scroll3 = new JScrollPane(tree3);
+			    scroll3.setPreferredSize(new Dimension(600,600));
+			    if (comm_bool == true)
+				{
+				    scroll4 = new JScrollPane(tree4);
+				    scroll4.setPreferredSize(new Dimension(600,600));
+				}
+			}
+		    else
+			{
+			    scroll = new JScrollPane(tree);
+			    scroll.setPreferredSize(new Dimension(600,600));
+			    if (comm_bool == true)
+				{
+				    scroll2 = new JScrollPane(tree2);
+				    scroll2.setPreferredSize(new Dimension(600,600));
+				}
+			}
+		}
+	    else
+		{
+		    if (new_tree == true)
+			{
+			    parentNode3 = (DefaultMutableTreeNode) parentNode3.getParent();
+			    
+			    if ((comm_bool == true)&&(comm_start == true))
+				{
+				    if (parentNode4 == null)  return;
+				    parentNode4 = (DefaultMutableTreeNode) parentNode4.getParent();
+				}  
+			}
+		    else
+			{
+			    parentNode = (DefaultMutableTreeNode) parentNode.getParent();
+			    
+			    if ((comm_bool == true)&&(comm_start == true))
+				{
+				    if (parentNode2 == null)  return;
+				    parentNode2 = (DefaultMutableTreeNode) parentNode2.getParent();
+				}  
+			}
+		}
+	}
+	
+	/*public BufferedImage iconToImage(Icon icon, Component targetComponent) 
+	{
+	    int w = icon.getIconWidth();
+	    int h = icon.getIconHeight();
+	    GraphicsConfiguration gc = targetComponent.getGraphicsConfiguration();
+	    BufferedImage image = gc.createCompatibleImage(w, h);
+	    Graphics2D g = image.createGraphics();
+	    icon.paintIcon(targetComponent,g,0,0);
+	    g.dispose();
+	    return image;
+	}
+	
+	public BufferedImage savingImage (ImageIcon img)
+	{
+	    JFrame f = new JFrame();
+	    f.setVisible(false);
+	    BufferedImage image = iconToImage(img,f);
+	    f.dispose();
+	    return image;
+	    }*/
+	
+	/**
+	 * This will add the image filename,width and height to the tree.
+	 * @param filename image's filename
+	 * @param width    image's width
+	 * @param height   image's height
+	 */
+	public void saveImage (String filename,String width,String height)
+	{
+	    startElement(messages.getString("Title"));
+	    characters(filename.toCharArray(),0,filename.length());
+	    endElement(messages.getString("Title"));
+	
+	    startElement(messages.getString("Size"));
+	    
+	    startElement(messages.getString("Width"));
+	    characters(width.toCharArray(),0,width.length());
+	    endElement(messages.getString("Width"));
+	    
+	    startElement(messages.getString("Height"));
+	    characters(height.toCharArray(),0,height.length());
+	    endElement(messages.getString("Height"));
+
+	    endElement(messages.getString("Size"));
+
+	    /*JFrame f = new JFrame();
+	    f.setVisible(false);
+	    BufferedImage image = savingImage(img);
+	    BufferedImage bi = new BufferedImage(400, 400, BufferedImage.TYPE_INT_RGB);
+	    Graphics2D big = bi.createGraphics();
+	    big.drawImage(image.getScaledInstance(400,400,Image.SCALE_SMOOTH),0,0,f);
+	    f.dispose();
+	    
+	
+	    JLabel label = new JLabel(new ImageIcon(bi));
+	    label.setPreferredSize(new Dimension(400,400));
+	    currNode = new DefaultMutableTreeNode(label);
+	    treeModel.insertNodeInto(currNode,parentNode,parentNode.getChildCount());*/
+	}
+	
+	/**
+	 * This method will add the characters to the current node's type.
+	 * @param ch is the char[] that is need to be added to the current node's type.
+	 * @param start is where we should start taking from ch.
+	 * @param len is the length of charcacters we should take from ch
+	 */
+	public void characters (char[] ch, int start, int len)
+	{
+	    String text;
+	    text = new String(ch,start,len);
+	    String text1;
+	    text1 = text.trim();
+	    if (text1.length() > 0)
+		{
+		    if ((comm_bool == true)&&(comm_start == true))
+			{
+			    if (new_tree == true)
+				{
+				    String txt2;
+				    txt2 = (String) currNode4.getUserObject();
+				    currNode4.setUserObject(txt2 + text1);
+				}
+			    else 
+				{
+				    String txt2;
+				    txt2 = (String) currNode2.getUserObject();
+				    currNode2.setUserObject(txt2 + text1);
+				}
+			}
+
+		    if (new_tree == true)
+			{
+			    String txt;
+			    txt = (String) currNode3.getUserObject();
+			    currNode3.setUserObject(txt + text1);
+			}
+		    else
+			{
+			    String txt;
+			    txt = (String) currNode.getUserObject();
+			    currNode.setUserObject(txt + text1);
+			}
+		}
+	}
+    }
+ 
+}
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SaveToXML.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SaveToXML.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SaveToXML.java	(revision 31635)
@@ -0,0 +1,482 @@
+package org.greenstone.gatherer.feedback;
+
+import java.awt.image.*;
+import java.io.*;// SAX classes.
+import javax.imageio.*;
+import org.xml.sax.*;
+import org.xml.sax.helpers.*;//JAXP 1.1
+import javax.xml.parsers.*;
+import javax.xml.transform.*;
+import javax.xml.transform.stream.*;
+import javax.xml.transform.sax.*; 
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.text.MessageFormat;
+import java.util.*;
+
+/**
+ * This class will make xml files from the information given.
+ * @author Veronica Liesaputra
+ */
+public class SaveToXML
+{
+    /**
+     * This is the file where the xml file will be saved.
+     */
+    private FileOutputStream fos;
+    /**
+     * This is the transformer handler for the xml file.
+     */
+    private TransformerHandler hd;
+    /**
+     * This is the attribute implementation for the xml file.
+     */
+    private AttributesImpl atts;
+     /**
+     * This variable will hold the resource of the words that is stored in Messages.properties file.
+     * The calling using messages.getString(someString) will caused someString to be translated
+     * into some other string that is hold in that file.usually it will caused it to be translated
+     * to the language that the user use or choose to have as stated in Locale.
+     */
+    private static ResourceBundle messages;
+
+    /**
+     * This constructor will setup what languange should be use.
+     * @param msg will hold the resource of the words that is stored in Messages.properties file.
+     */
+    public SaveToXML(ResourceBundle msg)
+    {
+	messages = msg;
+    }
+
+    /**
+     * This will save the image, image filename,width and height to the xml file.
+     * @param image    image
+     * @param filename image's filename
+     * @param width    image's width
+     * @param height   image's height
+     */
+    public void saveImage (String image,String filename,String width,String height)
+    {
+	try
+	    {
+		hd.startElement("","","Title",atts);
+		hd.characters(filename.toCharArray(),0,filename.length());
+		hd.endElement("","","Title");
+		hd.startElement("","","Size",atts);
+		hd.startElement("","","Width",atts);
+		hd.characters(width.toCharArray(),0,width.length());
+		hd.endElement("","","Width");
+		hd.startElement("","","Height",atts);
+		hd.characters(height.toCharArray(),0,height.length());
+		hd.endElement("","","Height");
+		hd.endElement("","","Size");
+		hd.startElement("","","View",atts);
+		hd.characters(image.toCharArray(),0,image.length());
+		hd.endElement("","","View");
+	    }
+	catch(SAXException sax) {}
+    }
+
+    /**
+     * This method will create and setup the xml file.
+     * @param xmlname this is the file name for the xml file.
+     * @param type    this is the first tag that open the xml file.
+     */
+    public void open (String xmlname,String type)
+    {
+	try
+	    {
+		File f;
+		f = new File(xmlname);
+		fos = new FileOutputStream(f);
+		PrintWriter out;
+		out = new PrintWriter(fos);
+		StreamResult streamResult;
+		streamResult = new StreamResult(out);
+		SAXTransformerFactory tf;
+		tf = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
+		hd = tf.newTransformerHandler();
+		Transformer serializer;
+		serializer = hd.getTransformer();
+		
+		serializer.setOutputProperty(OutputKeys.ENCODING,"UTF-8");
+		//serializer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,dtdname);
+		serializer.setOutputProperty(OutputKeys.INDENT,"yes");
+			
+		hd.setResult(streamResult);
+		atts = new AttributesImpl();
+		
+		hd.startDocument();
+		
+		hd.startElement("","",type,atts);   
+	    }
+	catch (IOException e){
+	    System.err.println ("Unable to write to file");
+	    System.exit(-1);
+	}catch (TransformerConfigurationException tce){
+	    System.err.println("Error in: TransformerConfigurationException");
+	}catch (SAXException spe) {
+	    // Error generated by the parser
+	    System.err.println("Error in: (SAXException");
+	    // Use the contained exception, if any
+	    Exception  x = spe;
+	    if (spe.getException() != null)
+		x = spe.getException();
+	    x.printStackTrace();
+	}
+    }
+
+    /**
+     * This method will setup to close the xml file.
+     * @param type    this is the last tag that close the xml file.
+     */
+    public void close(String type)
+    {
+	try
+	    {
+		hd.endElement("","",type);
+		hd.endDocument();
+		fos.close(); 
+	    }
+	catch (IOException e){
+	    System.err.println ("Unable to write to file");
+	    System.exit(-1);
+	}catch (SAXException spe) {
+	    // Error generated by the parser
+	    System.err.println("Error in: (SAXException");
+	    // Use the contained exception, if any
+	    Exception  x = spe;
+	    if (spe.getException() != null)
+		x = spe.getException();
+	    x.printStackTrace();
+	}
+    }
+
+    /**
+     * This method will add the open tag according to the number.
+     * @param num the number to say which type of tag it is.
+     */
+    public void startContent (int num)
+    {
+	try
+	    {
+		switch(num)
+		    {
+		    case 0: hd.startElement("","","COMPONENT",atts);
+			break;
+		    case 1: hd.startElement("","","Type",atts);
+			break;
+		    case 2: hd.startElement("","","Title",atts);
+			break;
+		    case 3: hd.startElement("","","Content",atts);
+			break;
+		    case 4: hd.startElement("","","Selected",atts);
+			break;
+		    case 5: hd.startElement("","","Visible",atts);  
+			break;
+		    case 6: hd.startElement("","","Image",atts);
+			break;
+		    case 7: hd.startElement("","","Date",atts);
+			break;
+		    case 8: hd.startElement("","","Command",atts);
+			break;
+		    case 9: hd.startElement("","","COMPONENTS",atts);
+			break;
+		    case 10:hd.startElement("","","LOG",atts);
+			break;
+		    case 11:hd.startElement("","","Icon",atts);
+			break;
+		    case 12:hd.startElement("","","ToolTipText",atts);
+			break;
+		    case 13:hd.startElement("","","Comment",atts);
+			break;
+		    }
+	    }
+	catch (SAXException spe) {
+	    // Error generated by the parser
+	    System.err.println("Error in: (SAXException");
+	    // Use the contained exception, if any
+	    Exception  x;
+	    x = spe;
+	    if (spe.getException() != null)
+		x = spe.getException();
+	    x.printStackTrace();
+	}
+    }
+
+    /**
+     * This method will add the text to the tag.
+     * @param text is the text to be saved in xml files.
+     */
+    public void saveContent (String text)
+    {
+	try
+	    {
+		if (text == null)
+		    text = "\t";
+		hd.characters(text.toCharArray(),0,text.length());
+	    }
+	catch (SAXException spe) {
+	    // Error generated by the parser
+	    System.err.println("Error in: (SAXException");
+	    // Use the contained exception, if any
+	    Exception  x;
+	    x = spe;
+	    if (spe.getException() != null)
+		x = spe.getException();
+	    x.printStackTrace();
+	}
+    }
+
+    /**
+     * This method will add the close tag according to the number.
+     * @param num the number to say which type of tag it is.
+     */
+    public void closeContent (int num)
+    {
+	try
+	    {
+		switch(num)
+		    {
+		    case 0: hd.endElement("","","COMPONENT");
+			break;
+		    case 1: hd.endElement("","","Type");
+			break;
+		    case 2: hd.endElement("","","Title");
+			break;
+		    case 3: hd.endElement("","","Content");
+			break;
+		    case 4: hd.endElement("","","Selected");
+			break;
+		    case 5: hd.endElement("","","Visible");
+			break;
+		    case 6: hd.endElement("","","Image");
+			break;
+		    case 7: hd.endElement("","","Date");
+			break;
+		    case 8: hd.endElement("","","Command");
+			break;
+		    case 9: hd.endElement("","","COMPONENTS");
+			break;
+		    case 10:hd.endElement("","","LOG");
+			break;
+		    case 11:hd.endElement("","","Icon");
+			break;
+		    case 12:hd.endElement("","","ToolTipText");
+			break;
+		    case 13:hd.endElement("","","Comment");
+			break;
+		    }
+	    }
+	catch (SAXException spe) {
+	    // Error generated by the parser
+	    System.err.println("Error in: (SAXException");
+	    // Use the contained exception, if any
+	    Exception  x;
+	    x = spe;
+	    if (spe.getException() != null)
+		x = spe.getException();
+	    x.printStackTrace();
+	}
+    }
+
+    /**
+     * This method save to xml all the information taken when user did the Feedback form.
+     * @param err_details is the information taken when user did the Feedback form.
+     * @param img         is the images taken when user did the Feedback form
+     */ 
+    public void saveFeedback (String[] err_details,String[] img)
+    {
+	try
+	    {
+		String dirname;
+		dirname = "xmlfeedback/";
+		FileOutputStream fos;
+		fos = new FileOutputStream(dirname + "feedback" + err_details[0] + 
+					   ".xml");
+		PrintWriter out;
+		out = new PrintWriter(fos);
+		StreamResult streamResult;
+		streamResult = new StreamResult(out);
+		SAXTransformerFactory tf;
+		tf = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
+		hd = tf.newTransformerHandler();
+		Transformer serializer;
+		serializer = hd.getTransformer();
+		serializer.setOutputProperty(OutputKeys.ENCODING,"UTF-8");
+		serializer.setOutputProperty(OutputKeys.INDENT,"yes");
+		hd.setResult(streamResult);
+		
+		
+		hd.startDocument();
+		atts = new AttributesImpl();
+		hd.startElement("","","BUGS",atts);
+		
+		hd.startElement("","","IDCode",atts);
+		hd.characters(err_details[0].toCharArray(),0,err_details[0].length());
+		hd.endElement("","","IDCode");
+		
+		hd.startElement("","","LastViewedWindow",atts);
+		hd.characters(err_details[1].toCharArray(),0,err_details[1].length());
+		hd.endElement("","","LastViewedWindow");
+
+		hd.startElement("","","Optionals",atts);
+		
+		hd.startElement("","","Type",atts);
+		hd.characters(err_details[3].toCharArray(),0,err_details[3].length());
+		hd.endElement("","","Type");
+		
+		hd.startElement("","","Urgency",atts);
+		hd.characters(err_details[4].toCharArray(),0,err_details[4].length());
+		hd.endElement("","","Urgency");
+		hd.endElement("","","Optionals");
+
+		hd.startElement("","","User",atts);
+		hd.startElement("","","Name",atts);
+		hd.characters(err_details[5].toCharArray(),0,err_details[5].length());
+		hd.endElement("","","Name");
+
+		hd.startElement("","","HomeDirectory",atts);
+		hd.characters(err_details[6].toCharArray(),0,err_details[6].length());
+		hd.endElement("","","HomeDirectory");
+
+		hd.startElement("","","WorkingDirectory",atts);
+		hd.characters(err_details[7].toCharArray(),0,err_details[7].length());
+		hd.endElement("","","WorkingDirectory");
+
+		hd.startElement("","","SMTP",atts);
+		hd.characters(err_details[19].toCharArray(),0,err_details[19].length());
+		hd.endElement("","","SMTP");
+
+		hd.startElement("","","Email",atts);
+		hd.characters(err_details[20].toCharArray(),0,err_details[20].length());
+		hd.endElement("","","Email");
+
+		hd.endElement("","","User");
+
+		hd.startElement("","","OpenFormTime",atts);
+		hd.characters(err_details[8].toCharArray(),0,err_details[8].length());
+		hd.endElement("","","OpenFormTime");
+
+		hd.startElement("","","SendFormTime",atts);
+		hd.characters(err_details[9].toCharArray(),0,err_details[9].length());
+		hd.endElement("","","SendFormTime");
+
+		hd.startElement("","","OperatingSystem",atts);
+		hd.characters(err_details[10].toCharArray(),0,err_details[10].length());
+		hd.endElement("","","OperatingSystem");
+
+		hd.startElement("","","Java",atts);
+
+		hd.startElement("","","JavaInformation",atts);
+		hd.characters(err_details[11].toCharArray(),0,err_details[11].length());
+		hd.endElement("","","JavaInformation");
+
+		hd.startElement("","","TotalMemory",atts);
+		hd.characters(err_details[21].toCharArray(),0,err_details[21].length());
+		hd.endElement("","","TotalMemory");
+
+		hd.startElement("","","MaxMemory",atts);
+		hd.characters(err_details[22].toCharArray(),0,err_details[22].length());
+		hd.endElement("","","MaxMemory");
+
+		hd.startElement("","","FreeMemory",atts);
+		hd.characters(err_details[23].toCharArray(),0,err_details[23].length());
+		hd.endElement("","","FreeMemory");
+
+		hd.startElement("","","DefaultLocale",atts);
+		hd.characters(err_details[12].toCharArray(),0,err_details[12].length());
+		hd.endElement("","","DefaultLocale");
+		
+		hd.endElement("","","Java");
+
+		hd.startElement("","","Browser",atts);
+		hd.characters(err_details[13].toCharArray(),0,err_details[13].length());
+		hd.endElement("","","Browser");
+
+		hd.startElement("","","LocalHostName",atts);
+		hd.characters(err_details[14].toCharArray(),0,err_details[14].length());
+		hd.endElement("","","LocalHostName");
+
+		hd.startElement("","","LocalHostAddress",atts);
+		hd.characters(err_details[15].toCharArray(),0,err_details[15].length());
+		hd.endElement("","","LocalHostAddress");
+
+		hd.startElement("","","ScreenResolution",atts);
+		hd.characters(err_details[16].toCharArray(),0,err_details[16].length());
+		hd.endElement("","","ScreenResolution");
+
+		if (img!= null)
+		    {
+			hd.startElement("","","Sequences",atts);
+			int i;
+			for ( i = 0 ; i < img.length ; i= i + 10)
+			    {
+				hd.startElement("","","Sequence",atts);
+				
+				hd.startElement("","","Title",atts);
+				hd.characters(img[i].toCharArray(),0,img[i].length());
+				hd.endElement("","","Title");
+				
+				hd.startElement("","","Details",atts);
+				hd.characters(img[i+9].toCharArray(),0,img[i+9].length());
+				hd.endElement("","","Details");
+				
+				hd.startElement("","","ImageDescription",atts);
+				
+				hd.startElement("","","ScreenShot",atts);
+				hd.startElement("","","Image",atts);
+				saveImage(img[i+1],img[i+2],img[i+7],img[i+8]);
+				hd.endElement("","","Image");
+				hd.endElement("","","ScreenShot");
+				
+				hd.startElement("","","ErrorLineAndScreenShot",atts);
+				hd.startElement("","","Image",atts);
+				saveImage(img[i+3],img[i+4],img[i+7],img[i+8]);
+				hd.endElement("","","Image");
+				hd.endElement("","","ErrorLineAndScreenShot");
+				
+				hd.startElement("","","ErrorLineForScreenShot",atts);
+				hd.startElement("","","Image",atts);
+				saveImage(img[i+5],img[i+6],img[i+7],img[i+8]); 
+				hd.endElement("","","Image");
+				hd.endElement("","","ErrorLineForScreenShot");
+				
+				hd.endElement("","","ImageDescription");
+				
+				hd.endElement("","","Sequence");
+			    }
+			hd.endElement("","","Sequences");
+		    }
+
+		hd.endElement("","","BUGS");
+		hd.endDocument();
+		fos.close();
+	    }
+	catch (IOException e){
+	    System.err.println ("Unable to write to file");
+	    System.exit(-1);
+	}catch (TransformerConfigurationException tce){
+	    System.err.println("Error in: TransformerConfigurationException");
+	}catch (SAXException spe) {
+	    // Error generated by the parser
+	    System.err.println("Error in: (SAXException");
+	    // Use the contained exception, if any
+	    Exception  x;
+	    x = spe;
+	    if (spe.getException() != null)
+		x = spe.getException();
+	    x.printStackTrace();
+	}
+    }
+}
+
+
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ScreenShot.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ScreenShot.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ScreenShot.java	(revision 31635)
@@ -0,0 +1,272 @@
+package org.greenstone.gatherer.feedback;
+
+import java.awt.*;
+import java.applet.Applet;
+import java.awt.image.*;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import java.awt.geom.GeneralPath;
+import java.io.*;
+import javax.imageio.*;
+import javax.swing.*; 
+import java.awt.geom.*;
+
+/**
+ * This class can take screen shot of the whole screen size or screen shot of a specified
+ * rectangle only.
+ * @author Veronica Liesaputra
+ */
+public class ScreenShot
+{
+    /**
+     * This is the buffered image that hold the screen shot.
+     */
+    private BufferedImage img;
+    
+    /**
+     * The dimension of the whole screen size.
+     */
+    private final static Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+    
+    /**
+     * Rectangle with a dimension of a whole screen size.
+     */
+    private Rectangle mRect = new Rectangle(dim);
+    
+    /**
+     * The desired width of the image.
+     */
+    private int iw;
+
+    /**
+     * The desired height of the image.
+     */
+    private int ih;
+
+    /**
+     * BufferedImage that hold the screen shot image with the desired height and width.
+     */
+    private BufferedImage bi;
+    
+    /**
+     * This is the robot that allows to take screen shot.
+     */
+    private Robot mRobot;
+  
+    /**
+     * This constructor will setup the robot to alow taking a screen shot.
+     */
+    public ScreenShot () 
+    {
+	try
+	    {
+		mRobot = new Robot();
+		iw = (int) (dim.getWidth() - 60);
+		ih = (int) (dim.getHeight() * 0.75) - 50;
+	    }
+	catch (AWTException exp) {}
+    }
+
+    /**
+     * This method will only take screen shot of the particular rectangle.
+     * @param rect is the specified rectangle that we want to take the screen shot of.
+     * @return     the image taken.
+     */
+    //public byte[] getImageIcon (Rectangle rect)
+    public BufferedImage getImageIcon (Rectangle rect)
+    {
+	BufferedImage image;
+	image = null;
+	
+	//try
+	//  {
+		img = mRobot.createScreenCapture(rect);
+		
+		int w,h;
+		boolean rescale;
+
+		rescale = false;
+		w = rect.width;
+		
+		if (w >= iw)
+		    {
+			w = iw - 50;
+			rescale = true;
+		    }
+		
+		h = rect.height;
+		
+		if ( h >= ih)
+		    {
+			h = ih  - 50;
+			rescale = true;
+		    }
+		
+		Graphics2D big;
+		big = null;
+		
+		if (rescale == true)
+		    {
+			image = new BufferedImage(w,h, BufferedImage.TYPE_INT_RGB);
+			big = image.createGraphics();
+			big.drawImage(img.getScaledInstance(w,h,Image.SCALE_SMOOTH),0,0,null);
+		    }
+		else
+		    {
+			image = img;
+		    }
+
+		
+		/*ByteArrayOutputStream stream;
+		stream = new ByteArrayOutputStream();
+		ImageIO.write(image,"jpeg",stream);
+		byte[] bytes;
+		bytes = stream.toByteArray();*/
+		
+		if (big != null)
+		    big.dispose();
+		//image = null;
+		//img = null;
+		ActionRecorderDialog.setSavefinish(true);
+		return image;
+		//return bytes;
+		//  }
+		//catch(IOException ex) {ex.printStackTrace();}
+	
+	//return null;
+    }
+
+    /**
+     * This method will only take screen shot of the particular rectangle and encode
+     * the image to its string representation.
+     * @param rect is the specified rectangle that we want to take the screen shot of.
+     * @return     the string representation of the image taken.
+     */
+    public String getImage (Rectangle rect)
+    {
+	String txt;
+	txt = null;
+
+	BufferedImage image;
+	image = null;
+	
+	try
+	    {
+		img = mRobot.createScreenCapture(rect);
+		
+		int w,h;
+		boolean rescale;
+
+		rescale = false;
+		w = rect.width;
+		
+		if (w >= iw)
+		    {
+			w = iw - 50;
+			rescale = true;
+		    }
+		
+		h = rect.height;
+		
+		if ( h >= ih)
+		    {
+			h = ih  - 50;
+			rescale = true;
+		    }
+	
+		Graphics2D big;
+		big = null;
+
+		if (rescale == true)
+		    {
+			image = new BufferedImage(w,h, BufferedImage.TYPE_INT_RGB);
+			big = image.createGraphics();
+			big.drawImage(img.getScaledInstance(w,h,Image.SCALE_SMOOTH),0,0,null);
+		    }
+		else
+		    {
+			image = img;
+		    }
+
+		ByteArrayOutputStream stream;
+		stream = new ByteArrayOutputStream();
+		ImageIO.write(image,"gif",stream);
+		byte[] bytes;
+		bytes = stream.toByteArray();
+		txt = Base64.encodeBytes(bytes);
+		
+		bytes = null;
+		if (big != null)
+		    big.dispose();
+		image = null;
+		img = null;
+	    }
+	catch(IOException ex) {ex.printStackTrace();}
+	
+	ActionRecorderDialog.setSavefinish(true);
+	
+	return txt;
+    }
+
+    /**
+     * This method will return the buffered image of the whole screen size screen shot
+     * with the desired width and height.
+     * @return the buffered image with the desired height and width.
+     */
+    public BufferedImage getBuffImage()
+    {
+	return bi;
+    }
+
+    /**
+     * This method will take screen shot of the whole screen and resize it to the
+     * desired width and height.
+     */
+    public BufferedImage captureScreen()
+    {
+	img = mRobot.createScreenCapture(mRect);
+	
+	Graphics2D big;
+	bi = new BufferedImage(iw,ih, BufferedImage.TYPE_INT_RGB);
+	big = bi.createGraphics();
+	big.drawImage(img.getScaledInstance(iw,ih,Image.SCALE_SMOOTH),0,0,null);
+	big.dispose();
+	big = null;
+
+	return bi;
+    }
+
+    /**
+     * This method will return the desired width.
+     * @return the desired width.
+     */
+    public int getWidth()
+    {
+	return iw;
+    }
+
+    /**
+     * This method will return the desired height.
+     * @return the desired height.
+     */
+    public int getHeight()
+    {
+	return ih;
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SelectPicture.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SelectPicture.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SelectPicture.java	(revision 31635)
@@ -0,0 +1,819 @@
+package org.greenstone.gatherer.feedback;
+
+import java.awt.image.*;
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.*;
+import javax.swing.event.MouseInputAdapter;
+import java.awt.*;
+import java.awt.geom.*;
+import java.awt.geom.Line2D.*;
+import java.awt.geom.Line2D.Double;
+import java.awt.event.*;
+import java.awt.event.MouseEvent;
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.text.MessageFormat;
+import javax.swing.border.*;
+import javax.swing.BorderFactory;
+
+import org.greenstone.gatherer.util.JarTools;
+
+/**
+ * This class allows user to make scribble lines in the image.
+ * This class will display an image to a label and if the user drag inside the label
+ * then it will draw coloured line as the user drag the mouse.
+ * Image here is the screen shot of the whole screen size.
+ * @author Veronica Liesaputra
+ */
+public class SelectPicture extends WindowAdapter
+{
+    /**
+     * This is the images that stored all the images for the whole screen size image.
+     */
+    private BufferedImage[] screen;
+  
+    /**
+     * This is the images that stored all the images for the window image.
+     */
+    private BufferedImage[] window;
+
+    /**
+     * This is the buffered image that contains the line that are drawn all
+     * over the screen image.
+     */
+    private BufferedImage bi;
+    
+    /**
+     * This is the buffered image of the line that are drawn all over the screen image.
+     */
+    private BufferedImage bi2;
+
+    /**
+     * This is the buffered image of the screen image.
+     */
+    private BufferedImage bi3;
+
+    /**
+     * This is the buffered image that contains the line that are drawn all over the window image.
+     */
+    private BufferedImage bi4;
+
+    /**
+     * This is the buffered image of the line that are drawn all over the window image.
+     */
+    private BufferedImage bi5;
+
+    /**
+     * This is the buffered image of the window image.
+     */
+    private BufferedImage bi6;
+
+    /**
+     * This is the window's x-coord in the screen image.
+     */
+    private int xcoord;
+
+    /**
+     * This is the window's y-coord in the screen image.
+     */
+    private int ycoord;
+
+    /**
+     * This is the window's width in the screen image.
+     */
+    private int width;
+
+    /**
+     * This is the window's height in the screen image.
+     */
+    private int height;
+    
+    /**
+     * This is to block all the input in this window while waiting.
+     */
+    private MouseListener mouse_blocker_listener = new MouseAdapter() {};
+    
+    /**
+     * This variable will hold the resource of the words that is stored in Messages.properties file.
+     * The calling using messages.getString(someString) will caused someString to be translated
+     * into some other string that is hold in that file.usually it will caused it to be translated
+     * to the language that the user use or choose to have as stated in Locale.
+     */
+    private static ResourceBundle messages;
+    
+    /**
+     * This variable hold the comments that user entered when they are drawing lines into the image.
+     */
+    private String details;
+    
+    /**
+     * This variable is a flag to say whether or not user wants to send the feedback straight away.
+     */
+    private boolean sendNow;
+
+    /**
+     * This variable is a flag to say whether the user wants to draw the scribble lines or
+     * to erase the image.
+     */
+    private boolean drawNow;
+
+    /**
+     * This variable is a flag to say whether or not the image user wants to send is window
+     * or the whole screen image.
+     */
+    private boolean windowNow;
+
+    /**
+     * This method will make a modal-dialog that will allow user to draw a line all over the image.
+     * @param sh the buffered images of the screen image.
+     * @param wh the buffered images of the window image.
+     * @param iswindow the flag to say whether user wants to send window or whole screen image.
+     * @param screen_details the comments that user typed for this screenshot.
+     * @param msg hold the resource of the words that is stored in Messages.properties file.
+     */
+    public SelectPicture(BufferedImage[] sh,BufferedImage[] wh,boolean iswindow,String screen_details,ResourceBundle msg)
+    {
+	windowNow = iswindow;
+	drawNow = true;
+	details = screen_details;
+	messages = msg;
+	window = wh;
+	screen = sh;
+    }
+    
+    /**
+     * This method setting up the window's bounds inside the screen image.
+     * @param x the window's x-coordinate in the screen image.
+     * @param y the window's y-coordniate in the screen image.
+     * @param w the window's width in the screen image.
+     * @param h the window's height in the screen image.
+     */
+    public void setWindowBounds (int x,int y,int w,int h)
+    {
+	xcoord = x;
+	ycoord = y;
+	width = w;
+	height = h;
+	makeNewWindow();
+    }
+
+    /**
+     * This method will get which one user wants to send the window or screen image.
+     * @return the flag whether user wants to send window or screen image. 
+     */
+    public boolean getIsWindow()
+    {
+	return windowNow;
+    }
+
+    /**
+     * This method will get the screen image of the screenshot with the line drawn all over it.
+     * @return  this is the screen image with the line drawn all over it.
+     */
+    public BufferedImage getImage()
+    {
+	return bi;
+    }
+
+    /**
+     * This method will get the screen image of the screenshot.
+     * @return  this is the screen image.
+     */
+    public BufferedImage getImage3()
+    {
+	return bi5;
+    }
+
+    /**
+     * This method will get the screen image of only the line that drawn all over the image of
+     * the screenshot.
+     * @return  this is the screen image of only the line.
+     */
+    public BufferedImage getImage2()
+    {
+	return bi2;
+    }
+
+    /**
+     * This method will get the window image of the screenshot with the line drawn all over it.
+     * @return  this is the image with the line drawn all over it.
+     */
+    public BufferedImage getWindowImage()
+    {
+	return bi3;
+    }
+
+    /**
+     * This method will get the window image of the screenshot.
+     * @return  this is the window image.
+     */
+    public BufferedImage getWindowImage3()
+    {
+	return bi6;
+    }
+
+    /**
+     * This method will get the window image of only the line that drawn all over the image of
+     * the screenshot.
+     * @return  this is the window image of only the line.
+     */
+    public BufferedImage getWindowImage2()
+    {
+	return bi4;
+    }
+
+    /**
+     * This method will give the comments user made when they are drawing line over the picture.
+     * @return this is the comment user made.
+     */
+    public String getDetails ()
+    {
+	return details;
+    }
+
+    public boolean getSendNow()
+    {
+	return sendNow;
+    }
+
+    /**
+     * This method will make a modal-dialog that will have the image of the screen shot in the label,
+     * allow user to draw a line all over the image and comments it as well. 
+     */
+    public void makeNewWindow()
+    {
+	JDialog f;
+	f = new Selecting(this);
+	f.setLocation(0,0);
+    }
+
+    /**
+     * This class will make a modal-dialog that will have the image of the screen shot in the label,
+     * allow user to draw a line all over the image and comments it as well. 
+     */
+    class Selecting extends JDialog implements ActionListener
+    {
+	/**
+	 * This is the modal-dialog.
+	 */
+	private JDialog frame;
+	
+	/**
+	 * This is the special label that allows user to draw a line all over the icon set in the label.
+	 */
+	private SelectionArea area;
+	
+	/**
+	 * This is the text area where user can enter the comments they want to add.
+	 */
+	private JTextArea problem_details;
+	
+	/**
+	 * This is the owner of this modal-dialog.
+	 */
+	private SelectPicture framework = null;
+	
+	/**
+	 * This is the graphs that will allow to draw the line all over the screen image in the label and getting
+	 * the image of it.
+	 */
+	private Graphs gp;
+
+	/**
+	 * This is the graphs that will allow to draw the line all over the window image in the label and getting
+	 * the image of it.
+	 */
+	private Graphs gp2;
+
+	/**
+	 * This constructor will make  a modal-dialog that will have the image of the screen shot in the label,
+	 * allow user to draw a line all over the image and comments it as well. 
+	 * @param controller this is the owner of the modal-dialog.
+	 */
+	public Selecting(SelectPicture controller)
+	{
+	    Container contentPane;
+   
+	    contentPane = getContentPane();
+
+	    gp = new Graphs(screen);
+	    
+	    window = gp.getWindowVersion(xcoord,ycoord,width,height);
+	  
+	    gp2 = new Graphs(window);
+
+	    buildUI(contentPane);
+	
+	    framework = controller;
+	   
+	    setTitle(messages.getString("Showwhereistheproblem"));
+	    setDefaultLookAndFeelDecorated(true);
+	    setModal(true);
+
+	    frame = this;
+	    addWindowListener(framework);
+	    setBackground(new Color(176,208,176)); 
+	    
+	    setLocation(0,0);
+	  
+	    pack();
+	    setVisible(true);
+	}
+
+	/**
+	 * This method will setup the contentpane of the modal-dialog.It will set the cursor for
+	 * the label to be from the file pen.gif and set the image icon for the label.
+	 * @param content this is the contentPane of the modal-dialog.
+	 */
+	private void buildUI(Container content) 
+	{
+	    final JPanel container;
+	    container = new JPanel();
+	    container.setLayout(new BoxLayout(container, BoxLayout.PAGE_AXIS));
+	    container.setBackground(new Color(176,208,176));
+	    container.setBorder(new EmptyBorder(5,5,5,5));
+
+	   
+	    ImageIcon image = createImageIcon(windowNow);
+	    area = new SelectionArea(image, this);
+	    area.setBackground(new Color(224,240,224));
+	    area.setBorder(BorderFactory.createCompoundBorder
+			   (BorderFactory.createLineBorder(Color.blue,4),
+			    new EmptyBorder(10,10,10,10)));
+	   
+	    ImageIcon imgicon,imgicon2;
+	    Image img,img2;
+	    imgicon = JarTools.getImage("pen.gif");
+	    img = imgicon.getImage();
+	    img = img.getScaledInstance(24,24,Image.SCALE_SMOOTH);
+	    imgicon2 = JarTools.getImage("eraserpen.gif");
+	    img2 = imgicon2.getImage();
+	    final Cursor cursor;
+	    final Cursor cursor2;
+	    cursor = Toolkit.getDefaultToolkit().createCustomCursor(img,new Point(0,0),"crayon");
+	    cursor2 = Toolkit.getDefaultToolkit().createCustomCursor(img2,new Point(0,0),"eraser");
+	    
+	    area.setCursor(cursor);
+
+	    Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+	    final JPanel iconarea = new JPanel();
+	    iconarea.setBackground(new Color(176,208,176));
+	    iconarea.add(area);
+	    container.add(iconarea);
+	    
+	    String screen_label;
+	    screen_label = " " + messages.getString("ScreenShotLabel");
+	    JLabel label;
+	    label = new JLabel(screen_label);
+	    label.setBackground(new Color(176,208,176));
+	    container.add(label);
+
+	    JPanel panes;
+	    panes = new JPanel();
+	    panes.setLayout(new GridLayout(0,2));
+	    panes.setBackground(new Color(176,208,176));
+
+	    problem_details = new JTextArea(details);
+	    problem_details.setWrapStyleWord(true);
+	    problem_details.setEditable(true);
+	    problem_details.setBackground(new Color(224,240,224));
+	    JScrollPane scroll;
+	    scroll = new JScrollPane(problem_details);
+	    scroll.setBackground(new Color(176,208,176));
+	    scroll.setPreferredSize(new Dimension(50,100));
+	    scroll.setBorder(BorderFactory.createTitledBorder(
+							      new EmptyBorder(0,0,0,0),
+							      "Screen shot " + 
+							      messages.getString("ProblemDetails")));
+
+	    panes.add(scroll);
+
+	    JPanel pane;
+	    pane = new JPanel();
+	    pane.setLayout(new GridLayout(2,3));
+	    pane.setBackground(new Color(176,208,176));
+	    pane.setBorder(new EmptyBorder(15,15,15,15));
+
+	    if ((window == null) || (screen == null)) {System.out.println("How come!");}
+	    else
+		{
+		    final JButton toggle;
+		    
+		    if (windowNow == false)
+			{
+			    toggle = new JButton("GLI window."); 
+			    toggle.setToolTipText("Showing only GLI window.");
+			}
+		    else
+			{
+			    toggle = new JButton("Whole screen.");
+			    toggle.setToolTipText("Showing whole screen.");
+			}
+		    toggle.setActionCommand("Toggle");
+		    toggle.addActionListener(new ActionListener ()
+			{
+			    public void actionPerformed (ActionEvent e)
+			    {
+				frame.addMouseListener(mouse_blocker_listener);
+				frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+				if (windowNow == true)
+				    {
+					windowNow = false;
+					toggle.setText("GLI window.");
+					toggle.setToolTipText("Showing only GLI window.");
+					window[0] = gp2.getScreenImage();
+					window[1] = gp2.getImage();
+					window[2] = gp2.getImage2();
+					gp.setScreenVersion(window,xcoord,ycoord,width,height);
+					area.setIcon(new ImageIcon(gp.getImage()));
+				    }
+				else
+				    {
+					windowNow = true;
+					toggle.setText("Whole screen.");
+					toggle.setToolTipText("Showing whole screen.");
+					
+					window[0].flush();
+					window[0] = null;
+					window[1].flush();
+					window[1] = null;
+					window[2].flush();
+					window[2] = null;
+					window = null;
+					BufferedImage tmp;
+					gp2.getImage().flush();
+					tmp = gp2.getImage();
+					tmp.flush();
+					tmp = null;
+					gp2.getImage2().flush();
+					tmp = gp2.getImage2();
+					tmp.flush();
+					tmp = null;
+					gp2.flush();
+					gp2 = null;
+					System.gc();
+
+					window = gp.getWindowVersion(xcoord,ycoord,width,height);
+					gp2 = new Graphs(window);
+					area.setIcon(new ImageIcon(gp2.getImage()));
+				    }
+
+				frame.pack();
+				frame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+				frame.removeMouseListener(mouse_blocker_listener);	
+			    }
+			});
+		    toggle.setBackground(new Color(176,208,176));
+		    pane.add(toggle);
+		}
+				     
+
+	    JButton pen;
+	    pen = new JButton(JarTools.getImage("penicon.gif"));
+	    pen.setText("Pen");
+	    pen.setPreferredSize(new Dimension(30,20));
+	    pen.setActionCommand("Pen");
+	    pen.setToolTipText("Draw.");
+	    pen.addActionListener(new ActionListener ()
+		{
+		    public void actionPerformed (ActionEvent e)
+		    {
+			area.setCursor(cursor);
+			drawNow = true;
+		    }
+		});
+	    pen.setBackground(new Color(176,208,176));
+	    pane.add(pen);
+
+	    JButton eraser;
+	    eraser = new JButton(JarTools.getImage("eraser.gif"));
+	    eraser.setText("Eraser");
+	    eraser.setActionCommand("Eraser");
+	    eraser.setPreferredSize(new Dimension(30,20));
+	    eraser.setToolTipText("Erase.");
+	    eraser.addActionListener(new ActionListener ()
+		{
+		    public void actionPerformed (ActionEvent e)
+		    {
+			area.setCursor(cursor2);
+			drawNow = false;
+		    }
+		});
+	    eraser.setBackground(new Color(176,208,176));
+	    pane.add(eraser);
+
+	    if ((window == null) || (screen == null)) 
+		{
+		    JLabel empty = new JLabel(" ");
+		    empty.setBackground(new Color(176,208,176));
+		    pane.add(empty);
+		}
+
+	    JButton button;
+	    button = new JButton("Clear Scribble");
+	    button.setActionCommand("Clear");
+	    button.setToolTipText("Clear all the scribble lines.");
+	    button.addActionListener(this);
+	    button.setBackground(new Color(176,208,176));
+	    pane.add(button);
+
+	    JButton button2;
+	    button2 = new JButton("Finish Scribble");
+	    button2.setActionCommand(messages.getString("Send"));
+	    button2.setToolTipText("Back to Feedback form.");
+	    button2.addActionListener(this);
+	    button2.setBackground(new Color(176,208,176));
+	    pane.add(button2);
+
+	    JButton button3;
+	    button3 = new JButton("Send");
+	    button3.setActionCommand("SendNow");
+	    button3.setToolTipText("Finish reporting feedback and send all information now.");
+	    button3.addActionListener(this);
+	    button3.setBackground(new Color(176,208,176));
+	    pane.add(button3);
+
+	    panes.add(pane);
+
+	    container.add(panes);
+	    
+	    panes.setAlignmentX(Component.LEFT_ALIGNMENT);
+	    label.setAlignmentX(Component.LEFT_ALIGNMENT);
+	    iconarea.setAlignmentX(Component.LEFT_ALIGNMENT);
+	   
+	    container.setSize(container.getPreferredSize());
+	    content.add(container);
+	}
+
+	/**
+	 * This method will tell what the program should do when user click the buttons.
+	 * @param e this is the event when user click button.
+	 */
+	public void actionPerformed (ActionEvent e)
+	{
+	    if(messages.getString("Send").equals(e.getActionCommand()))
+		{
+		    frame.addMouseListener(mouse_blocker_listener);
+		    frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+		    sendNow = false;
+		    if (windowNow == false)
+			{
+			    window = gp.getWindowVersion(xcoord,ycoord,width,height);
+			}
+		    else
+			{
+			    window[0] = gp2.getScreenImage();
+			    window[1] = gp2.getImage();
+			    window[2] = gp2.getImage2();
+			    gp.setScreenVersion(window,xcoord,ycoord,width,height);
+			}
+ 
+		    bi5 = gp.getScreenImage();
+		    bi = gp.getImage();
+		    bi2 = gp.getImage2();
+		    bi3 = window[1];
+		    bi4 = window[2];
+		    bi6 = window[0];
+
+		    details = problem_details.getText();
+		    frame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+		    frame.removeMouseListener(mouse_blocker_listener);	
+		    dispose();
+		}
+	    if ("Clear".equals(e.getActionCommand()))
+		{
+		    if (windowNow == false)
+			{
+			    gp.reset();
+			    area.setIcon(new ImageIcon(gp.getScreenImage()));
+			}
+		    else
+			{
+			    gp2.reset();
+			    area.setIcon(new ImageIcon(gp2.getScreenImage()));
+			}
+		}
+
+	    if ("SendNow".equals(e.getActionCommand()))
+		{
+		    frame.addMouseListener(mouse_blocker_listener);
+		    frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+		    sendNow = true;
+
+		    if (windowNow == false)
+			{
+			    window = gp.getWindowVersion(xcoord,ycoord,width,height);
+			}
+		    else
+			{
+			    window[0] = gp2.getScreenImage();
+			    window[1] = gp2.getImage();
+			    window[2] = gp2.getImage2();
+			    gp.setScreenVersion(window,xcoord,ycoord,width,height);
+			}
+ 
+		    bi5 = gp.getScreenImage();
+		    bi = gp.getImage();
+		    bi2 = gp.getImage2();
+		    bi3 = window[1];
+		    bi4 = window[2];
+		    bi6 = window[0];
+
+		    details = problem_details.getText();
+		    frame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+		    frame.removeMouseListener(mouse_blocker_listener);	
+		    dispose();
+		}
+	}
+
+	/**
+	 * This method will create ImageIcon of the buffered image get from the screen shot instance.
+	 * The image is screen shot whole screen size image.
+	 * @return ImageIcon of the screen shot image.
+	 */
+	protected ImageIcon createImageIcon(boolean iswindow)
+	{
+	    if (iswindow == false)
+		{
+		    if (screen[1] == null)
+			return new ImageIcon(screen[0]);
+		    else
+			return new ImageIcon(screen[1]);
+		}
+	    else
+		{
+		    if (window[1] == null)
+			return new ImageIcon(window[0]);
+		    else
+			return new ImageIcon(window[1]);
+		}
+	}
+	
+	/**
+	 * This is the special class, that will allows user to draw a line all over the label when 
+	 * user dragged the mouse inside the area.
+	 */
+      	private class SelectionArea extends JLabel 
+	{
+	    /**
+	     * This is the owner of this label.
+	     */
+	    private Selecting controller;
+	    /**
+	     * This is the x coordinate of the first point.
+	     */
+	    private int x1;
+	    /**
+	     * This is the x coordinate of the second point.
+	     */
+	    private int x2;
+	    /**
+	     * This is the y coordinate of the first point.
+	     */
+	    private int y1;
+	    /**
+	     * This is the y coordinate of the second point.
+	     */
+	    private int y2;
+	    /**
+	     * This is the flag to say that user is over the label of the screen image 
+	     * and they are doing the mouse input from there.
+	     */
+	    private boolean ismouse;
+
+	    /**
+	     * This method will create a label with the specified image 
+	     * icon and it will added with mouse listener that will
+	     * allow user to draw a line all over the image.
+	     * @param image image icon to be added to the label.
+	     * @param controller this is the owner of this label.
+	     */
+	    public SelectionArea(ImageIcon image, Selecting controller) 
+	    {
+		super(image);
+		this.controller = controller;
+		setOpaque(true);
+		
+		MyListener myListener;
+		myListener = new MyListener();
+		addMouseListener(myListener);
+		addMouseMotionListener(myListener);
+	    } 
+	    
+	    /**
+	     * This is the modified mouse listener that will allow user to draw
+	     * a line all over the label while user dragging the mouse.
+	     */
+	    private class MyListener extends MouseInputAdapter 
+	    {
+		/**
+		 * This method will make a red dot in the place where user pressed the mouse.
+		 * @param e the mouse event
+		 */
+		public void mousePressed(MouseEvent e) 
+		{
+		    int x;
+		    x = e.getX();
+		    int y;
+		    y = e.getY();
+		
+		    x2 = x;
+		    y2 = y;
+		    
+		    x1 = x2;
+		    y1 = y2;
+		}
+
+		/**
+		 * This method will make a red line from (x1,y1) to the coordinate where
+		 * the mouse is located now.
+		 * @param e the mouse event.
+		 */
+		public void mouseDragged(MouseEvent e) 
+		{
+		    ismouse = true;
+		    updateSize(e);
+		}
+
+		
+		/**
+		 * This method will make a red line from (x1,y1) to the coordinate where
+		 * the mouse is located now.
+		 * @param e the mouse event.
+		 */
+		public void updateSize(MouseEvent e) 
+		{
+		    int x;
+		    x = e.getX();
+		    int y;
+		    y = e.getY();
+		
+		    x2 = x;
+		    y2 = y;
+	
+		    repaint();    
+		}
+	    }
+	    
+	    /**
+	     * This method will repaint all the component inside the dialog-window.
+	     * It will draw a red line from (x1,y1) to (x2,y2) coordinates and
+	     * updates the value of x1,y1 and the icon for the label. 
+	     * @param g the graphics of this dialog-window.
+	     */
+	    public void paintComponent(Graphics g) 
+	    {
+		super.paintComponent(g);
+
+		if (ismouse == true)
+		    {
+			if (windowNow == false)
+			    {
+				if (drawNow == true)
+				    gp.drawLines(x1,y1,x2,y2);
+				else
+				    gp.eraseLines(x1,y1,x2,y2);
+			    }
+			else
+			    {
+				if (drawNow == true)
+				    gp2.drawLines(x1,y1,x2,y2);
+				else
+				    gp2.eraseLines(x1,y1,x2,y2);
+			    }
+			
+			x1 = x2;
+			y1 = y2;
+
+			if (windowNow == false)
+			    {
+				area.setIcon(new ImageIcon(gp.getImage()));
+			    }
+			else
+			    {
+				area.setIcon(new ImageIcon(gp2.getImage()));
+			    }
+			
+			ismouse = false;
+		    }
+	    }
+	}
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SendHTTP.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SendHTTP.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SendHTTP.java	(revision 31635)
@@ -0,0 +1,421 @@
+package org.greenstone.gatherer.feedback;
+
+import java.util.Properties;
+import java.net.*;
+import java.io.*;
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.text.MessageFormat;
+import javax.swing.*;
+import java.awt.Dimension;
+
+/**
+ * This class will send the 2 jar files to the server using HTTP Post.
+ */
+public class SendHTTP
+{
+    /**
+     * This is variable will hold the arguments provided by user in order to send 
+     */
+    private String[] argument;
+    /**
+     * This variable will hold the resource of the words that is stored in Messages.properties file.
+     * The calling using messages.getString(someString) will caused someString to be translated
+     * into some other string that is hold in that file.usually it will caused it to be translated
+     * to the language that the user use or choose to have as stated in Locale.
+     */
+    private static ResourceBundle msg;
+    /**
+     * This variable will hold the jar file unique code.
+     * For example: Jar123File.jar then file_code = 123
+     */
+    private String file_code;
+    /**
+     * This is the flag that will say whether or not the sending is successful.
+     * If failed = false means its succesful otherwise its failed.
+     */
+    private boolean failed = false;
+
+    /**
+     * Crates instance of SendHTTP
+     */
+    public SendHTTP () {}
+  
+    /**
+     * This method will send 2 files back to the server.
+     * It will also send  the code if the sending is failed then it will give user a message saying whats
+     * the possible error of it, and it will make the jar file changed into
+     * Unsend_Jar123File.jar file.
+     * If the sending is succesful then it will try to send all the other jar files
+     * that were failed to send before.And then it will deletes all the jar files
+     * that are send successfully.
+     * @param args the argument provided to send the email.
+     * @param messages hold the resource of the words that is stored in Messages.properties file.
+     * @param code the code of the email to be send.
+     */
+    public void  sendMail(String[] args,ResourceBundle messages,String code) 
+    {
+	argument = args;
+	msg = messages;
+	file_code = code;
+
+	try
+	    {
+		String BND = "---------------------------7d021a37605f0";
+		String hostURL =  "http://kanuka.cs.waikato.ac.nz:8090/glifeedback/Upload";
+
+		URL mURL = new URL(hostURL+"?name=posting");
+		URLConnection conn = mURL.openConnection();
+		
+		if (conn instanceof HttpURLConnection)
+		    ((HttpURLConnection) conn).setRequestMethod("POST");
+		 
+		conn.setDoInput(true);
+		conn.setDoOutput(true);
+		conn.setUseCaches(false);
+		conn.setDefaultUseCaches(false);
+		conn.setRequestProperty("Accept","*/*");
+		conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BND);
+		conn.setRequestProperty("Connection","Keep-Alive");
+		conn.setRequestProperty("Cache-Control","no-cache");
+
+		DataOutputStream out;
+		out = new DataOutputStream(conn.getOutputStream());
+
+		//--- Sending subject -------
+		String txt;
+		txt = "--" + BND + "\r\n";
+		out.writeBytes(txt);
+
+		txt = "Content-Disposition: form-data; ";
+		out.writeBytes(txt);
+
+		String subject;
+		subject = "Send Feedback" + code;
+		txt = "name=\"" + "Subject" + "\"\r\n"; 
+		out.writeBytes(txt);
+		
+		txt = "\r\n";
+		out.writeBytes(txt);
+
+		txt = subject + "\r\n";
+		out.writeBytes(txt);
+		
+		out.flush();
+
+		//--- Sending text ------
+		txt = "--" + BND + "\r\n";
+		out.writeBytes(txt);
+
+		txt = "Content-Disposition: form-data; ";
+		out.writeBytes(txt);
+
+		String attmnt;
+		attmnt = "We have succesfully received your feedback report.\n" +
+		    "To check what files we have received go to this link: " + hostURL +
+		    "/?name=" + code + "\n" + 
+		    "Please keep referencing to this ID Code for this feedback report.\n" +
+		    "ID Code: " + code;
+
+		txt = "name=\"" + "Text" + "\"\r\n";
+		out.writeBytes(txt);
+		
+		txt = "\r\n";
+		out.writeBytes(txt);
+		
+		txt = attmnt + "\r\n";
+		out.writeBytes(txt);
+
+		out.flush();
+
+		//--------- Sending xml-jar file
+		FileInputStream fis;
+		File f;
+		String name,path;
+		
+		f = new File(args[1]);
+		
+		txt = "--" + BND + "\r\n";
+		out.writeBytes(txt);
+
+		txt = "Content-Disposition: form-data; ";
+		out.writeBytes(txt);
+		
+		name = f.getName();
+		txt = "name=\"" + name + "\"; ";
+		out.writeBytes(txt);
+		
+		path = f.getAbsolutePath();
+		txt = "filename=\"" + path + "\"\r\n";
+		out.writeBytes(txt);
+
+		txt = "Content-Type: aplication/octet-stream\r\n";
+		out.writeBytes(txt);
+
+		txt = "\r\n";
+		out.writeBytes(txt);
+
+		fis = new FileInputStream(f);
+		byte[] data = new byte[1024];		
+		int r = 0;
+		while((r = fis.read(data, 0, data.length)) != -1) 
+		    {			
+			out.write(data, 0, r);		
+		    }
+		fis.close();
+
+		txt = "\r\n";
+		out.writeBytes(txt);
+
+		out.flush();
+
+		//--- Sending parser.jar ----
+		txt = "--" + BND + "\r\n";
+		out.writeBytes(txt);
+		
+		txt = "Content-Disposition: form-data; ";
+		out.writeBytes(txt);
+		
+		f = new File(args[4]);
+		
+		name = f.getName();
+		txt = "name=\"" + name + "\"; ";
+		out.writeBytes(txt);
+		
+		path = f.getAbsolutePath();
+		txt = "filename=\"" + path + "\"\r\n";
+		out.writeBytes(txt);
+
+		txt = "Content-Type: application/octet-stream\r\n";
+		out.writeBytes(txt);
+
+		txt = "\r\n";
+		out.writeBytes(txt);
+
+		fis = new FileInputStream(f);
+	        data = new byte[1024];		
+		r = 0;
+		while((r = fis.read(data, 0, data.length)) != -1) 
+		    {			
+			out.write(data, 0, r);		
+		    }
+		fis.close();
+		
+		txt = "\r\n";
+		out.writeBytes(txt);
+
+		out.flush();
+
+		//----- finished sending -----
+		txt = "--" + BND + "--";
+		out.writeBytes(txt);
+
+		out.flush();
+		out.close();
+
+		//----- getting the response ------
+		BufferedReader reader;
+		reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+		String responseLine;
+		responseLine = null;
+
+		StringBuffer response;
+		response = new StringBuffer();
+		while ((responseLine = reader.readLine()) != null)
+		    response.append(responseLine+"\n");
+
+		reader.close();
+	 
+
+		//------ our application's response to server's response
+		/*if (response.equals("failed"))
+		    {
+			failed = true;
+		
+			JOptionPane.showMessageDialog(null,messages.getString("InternalError")+ "\n" + 
+						      messages.getString("Emaillater"),messages.getString("Error"),
+						      JOptionPane.ERROR_MESSAGE);
+			//Conformation.setFinish(true);
+		    }
+		else if (response.equals("success"))
+		    failed = false;
+		else
+		{*/
+			failed = false;
+			JDialog.setDefaultLookAndFeelDecorated(false);
+			JDialog dlg = new JDialog();
+			JLabel lbl = new JLabel (response.toString());
+			
+			dlg.getContentPane().add(lbl);
+			dlg.pack();
+			dlg.setVisible(true);
+			// }
+
+		//Conformation.setFinish(true);
+	    }
+	catch (MalformedURLException exp) 
+	    {
+		failed = true;
+		
+		JOptionPane.showMessageDialog(null,"malformed URL" + "\n" + 
+					      messages.getString("Emaillater"),messages.getString("Error"),
+					      JOptionPane.ERROR_MESSAGE); 
+		System.out.println("malformed URL"); 
+	    }
+	catch (IOException e) 
+	    { 
+		failed = true;
+		
+		JOptionPane.showMessageDialog(null,messages.getString("InternalError")+ "\n" + 
+					      messages.getString("Emaillater"),messages.getString("Error"),
+					      JOptionPane.ERROR_MESSAGE);e.printStackTrace();
+	    }
+
+	String dirname;
+	dirname = "xmlfeedback/";
+	
+	if (failed == false)
+	    {
+		File f;
+		f = new File(dirname + "Jar" + file_code + "File.jar");
+		f.delete();
+		
+		File file;
+		file = new File(dirname + ".");
+		File[] fileList;
+		fileList = file.listFiles();
+		
+		int y;
+		for (y = 0 ; y < fileList.length ; y++)
+		    {
+			String filename;
+			filename = isRightJarFile(fileList[y]);
+			if (filename != null)
+			    {
+				renameTo(fileList[y],new File(filename));
+				argument[1] = filename;
+				sendMail(argument,msg,getCode(filename));
+			    }
+		    }
+	    }
+	else
+	    {
+		File f2;
+		f2 = new File(dirname + "Jar" + file_code + "File.jar");
+		renameTo(f2,new File(dirname + "Unsend_Jar" + file_code + "File.jar"));
+	    }
+	
+    }
+    
+    /**
+     * This method will rename file.
+     * @param src is the name of the file to be renamed.
+     * @param dest is the desired file name.
+     */
+    private void renameTo (File src, File dest)
+    {
+	if (src.renameTo(dest) == false)
+	    {
+		try
+		    {
+			FileInputStream in;
+			in = new FileInputStream(src);
+			FileOutputStream out;
+			out = new FileOutputStream(dest);
+			int next;
+			
+			while (true)
+			    {
+				next = in.read();
+				if (next == -1)
+				    break;
+				else
+				    out.write((byte) next);
+			    }
+			
+			in.close();
+			out.close();
+		    }
+		catch (IOException exp) {exp.printStackTrace();}
+	    }
+    }
+
+    /**
+     * This method will get the extension of the file.
+     * @param f is the file that we want to get the extension.
+     * @return  the file extension
+     */
+    private String getExtension(File f)
+    {
+	String ext;
+	ext = null;
+	String s;
+	s = f.getName();
+	int i;
+	i = s.lastIndexOf('.');
+	
+	if (i > 0 &&  i < s.length() - 1)
+	    {
+		ext = s.substring(i+1).toLowerCase();
+	    }
+	return ext;
+    }
+    
+    /**
+     * This method will get all the name of the file after "_" if the file its a jar file.
+     * For example Unsend_Jar123File.jar then this method will return Jar123File, otherwise
+     * file with any other format will return null.
+     * @param f is the file that we want to get the name.
+     */
+    private String isRightJarFile(File f)
+    {
+	String ext;
+	ext = null;
+	
+	if (f.isDirectory() == true)
+	    return null;
+	
+	if (getExtension(f).compareTo("jar") == 0)
+	    {
+		String s;
+		s = f.getName();
+		int i;
+		i = s.lastIndexOf("_");
+
+		if (i > 0 &&  i < s.length() - 1)
+		    {
+			ext = s.substring(i+1,s.length());
+		    }
+		else
+		    ext = null;
+		
+		return ext;
+	    }
+	else
+	    return null;
+    }
+    
+    /**
+     * This method will getting the code that inside the Jar filename.
+     * For example Jar123File then this method will return 123. 
+     */
+    private String getCode (String s)
+	{
+	    int i;
+	    i = s.lastIndexOf("Jar");
+	    int j;
+	    j = s.indexOf("File");
+	    String code;
+	    code = s.substring((i+3),j);
+	    return code;
+	}
+}
+
+
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SeparateGraphs.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SeparateGraphs.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/SeparateGraphs.java	(revision 31635)
@@ -0,0 +1,105 @@
+/** This class is not currently used and therefore will not be compiled by makegli. */
+
+package org.greenstone.gatherer.feedback;
+
+import java.io.*;
+import javax.imageio.*;
+import java.awt.*;
+import java.awt.image.*;
+import com.sun.image.codec.jpeg.*;
+import javax.swing.*;
+
+/**
+ * This class will separate a image file into 2 jpeg image files by taking
+ * all the point in that image file that has the color red and save all that
+ * points to another image file, while all the other point to another image
+ * file.
+ * @author Veronica Liesaputra
+ */
+public class SeparateGraphs
+{
+    /**
+     * This is the BufferedImage that we want to separate.
+     */
+    private BufferedImage bi;
+
+    /**
+     * This constructor will save the image inside the file with the specified 
+     * filename to the BufferedImage.
+     */
+    public SeparateGraphs(String filename)
+    {
+	try
+	    {
+		File f = new File(filename);
+		bi = ImageIO.read(f);
+	    }
+	catch(IOException exp) {}
+    }
+
+    /**
+     * This method will separate bi into 2 image and save it to
+     * jpeg files with the specified filename.
+     * The 2 image separated will be, 1 image is the image of all the points in bi that
+     * have the color red and another image is the image of all other points in bi that
+     * do not have the color red.
+     * @param file1 this is the filename for the image of all other points that do not have color red.
+     * @param file2 this is the filename for the image of all points that have the color red.
+     */
+    public void separate(String file1,String file2)
+    {
+	BufferedImage bi1,bi2;
+	
+	int width = bi.getWidth();
+	int height = bi.getHeight();
+	
+	bi1 = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
+	bi2 = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
+
+	for (int y = 0 ; y < height ; y++) 
+	    {
+		for (int x = 0 ; x < width ; x++)
+		    {
+			int rgb = bi.getRGB(x,y);
+			Color c = new Color(rgb,true);
+			if (c != Color.RED)
+			    {
+				bi1.setRGB(x,y,rgb);
+			    }
+			else
+			    {
+				bi2.setRGB(x,y,rgb);
+			    }
+		    }
+	    }
+
+	save(bi1,file1);
+	save(bi2,file2);
+    }
+
+    /**
+     * This method will save the BufferedImage into a jpeg file with the specified filenames.
+     * @param b is the BufferedImage to be saved.
+     * @param filename is the name of the jpeg files.
+     */
+    public void save(BufferedImage b,String filename)
+    {
+	try 
+	    {
+		File file = new File(filename);
+		FileOutputStream out = new FileOutputStream(file);
+	    
+		JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
+		JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(b);
+		param.setQuality(1.0f, false);
+		encoder.setJPEGEncodeParam(param);
+		encoder.encode(b);
+		
+	    } catch (Exception ex) {}
+    }
+}
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Separating.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Separating.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/Separating.java	(revision 31635)
@@ -0,0 +1,108 @@
+/** This class is not currently used and therefore will not be compiled by makegli. */
+
+package org.greenstone.gatherer.feedback;
+
+import java.io.*;
+import javax.imageio.*;
+import java.awt.*;
+import java.awt.image.*;
+import com.sun.image.codec.jpeg.*;
+import javax.swing.*;
+
+/**
+ * This method will separate 1 image file into 2 image  with the help of
+ * another file and sace the difference into a jpeg files.
+ * For example : We have 1 image file which is a screen shot and another image file
+ *               is the screenshot with some line in it.
+ *               This class will separate the screen shot and the line into 2 separates 
+ *               images.And then it will save the line into a jpeg files.
+ * 
+ * @author Veronica Liesaputra
+ */
+public class Separating
+{
+    /**
+     * This is the BufferedImage of image inside the reference image file.
+     */
+    private BufferedImage bi;
+    /**
+     * This is the BufferedImage of image that we want to separate into
+     * 2 different image using the reference file.
+     */
+    private BufferedImage src_bi;
+
+    /**
+     * It will read whats inside the 2 given filenames and put it in the
+     * right BufferedImage.
+     * @param filename1 this is the file name of the reference image file.
+     * @param filename2 this is the file name of the file we want to separate into 2.
+     */
+    public Separating(String filename1,String filename2)
+    {
+	try
+	    {
+		File f1 = new File(filename1);
+		File f2 = new File(filename2);
+		bi = ImageIO.read(f1);
+		src_bi = ImageIO.read(f2);
+	    }
+	catch(IOException exp) {}
+    }
+
+    /**
+     * This method will get the difference that exist between bi and src_bi and
+     * will save the difference inside a jpeg file with the specified file name.
+     * @param file1 is the name of the file where we want to save the image.
+     */
+    public void separate(String file1)
+    {
+	BufferedImage bi1;
+	
+	int width = bi.getWidth();
+	int height = bi.getHeight();
+	
+	bi1 = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
+
+	for (int y = 0 ; y < height ; y++) 
+	    {
+		for (int x = 0 ; x < width ; x++)
+		    {
+			int rgb = bi.getRGB(x,y);
+			int src_rgb = src_bi.getRGB(x,y);
+			
+			if (src_rgb != rgb)
+			    {
+				bi1.setRGB(x,y,rgb);
+			    }
+		    }
+	    }
+
+	save(bi1,file1);
+    }
+
+    /**
+     * This method will save a BufferedImage to a jpeg file with the specified filename.
+     * @param b is the BufferedImage to be saved.
+     * @param filename is the name of the jpeg file.
+     */
+    public void save(BufferedImage b,String filename)
+    {
+	try 
+	    {
+		File file = new File(filename);
+		FileOutputStream out = new FileOutputStream(file);
+	    
+		JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
+		JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(b);
+		param.setQuality(1.0f, false);
+		encoder.setJPEGEncodeParam(param);
+		encoder.encode(b);
+		
+	    } catch (Exception ex) {}
+    }
+}
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/UserComponent.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/UserComponent.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/UserComponent.java	(revision 31635)
@@ -0,0 +1,439 @@
+package org.greenstone.gatherer.feedback;
+
+import java.util.*;
+import java.awt.image.*;
+import java.io.*;
+import javax.imageio.*;
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * This class willl hold the information about a component and the contents inside the component.
+ * @author Veronica Liesaputra
+ */
+public class UserComponent implements Serializable
+{
+    /**
+     * This variable will hold what is the type of instance this component is.
+     */
+    private String type = null;
+
+    /**
+     * This variable will store the title of this component.
+     */
+    private String title = null;
+
+    /**
+     * This varaible will store the array list of the contents of this component.
+     */
+    private ArrayList content = null;
+
+    /**
+     * This variable will store the status of this component, whether or not
+     * this component is selected by the user.
+     */
+    private String selected = null;
+
+    /**
+     * This variable will store the status of this component, whether or not
+     * this component is visible.
+     */
+    private String visible = null;
+
+    /**
+     * This variable will hold the descrption text saying that there is no
+     * image in this component.
+     */
+    private String image = null;
+
+    /**
+     * This is the supposed filename of the image that is in this component should use if 
+     * we want to save it to a jpeg file.
+     */
+    private String img = null;
+
+    /**
+     * This is the string representation of the image that is in this component.
+     */
+    private String imgFile = null;
+
+    /**
+     * This is the index that will help storing the data to the correct data member.
+     */
+    private int index;
+
+    /**
+     * This is the height of the image that is in this component.
+     */
+    private String height;
+
+    /**
+     * This is the width of the image that is in this component.
+     */
+    private String width;
+
+    /**
+     * This variable will hold the tooltiptext set for this component.
+     */
+    private String tooltip;
+
+    /**
+     * This will initialized content to a new empty Arrray list.
+     */
+    public UserComponent()
+    {
+	content = new ArrayList();
+    }
+
+    /**
+     * This method will get the width of the image that is inside this component.
+     * @return image's width.
+     */
+    public String getWidth()
+    {
+	return width;
+    }
+
+    /**
+     * This method will get the height of the image that is inside this component.
+     * @return image's height.
+     */
+    public String getHeight()
+    {
+	return height;
+    }
+
+    /**
+     * This method will get the type of instance this component is.
+     * @return component's type.
+     */
+    public String getType()
+    {
+	return type;
+    }
+
+    /**
+     * This method will get the tooltiptext set for this component.
+     * @return component's tooltiptext.
+     */
+    public String getToolTip()
+    {
+	return tooltip;
+    }
+
+    /**
+     * This method will get the title for this component.
+     * @return component's title.
+     */
+    public String getTitle()
+    {
+	return title;
+    }
+
+    /**
+     * This method will get the content inside this component.
+     * @return component's content.
+     */
+    public ArrayList getContent()
+    {
+	return content;
+    }
+
+    /**
+     * This method will get whether or not this component is 
+     * selected by the user.
+     * @return component selected or not.
+     */
+    public String getSelected()
+    {
+	return selected;
+    }
+
+    /**
+     * This method will get whether or not this component is 
+     * visible.
+     * @return component visible or not.
+     */
+    public String getVisible()
+    {
+	return visible;
+    }
+
+    /**
+     * This method will get the descrption text saying that there is no
+     * image in this component.
+     * @return the description text.
+     */
+    public String getImage()
+    {
+	return image;
+    }
+
+    /**
+     * This method will get the supposed filename of the image that is in this component should use if 
+     * we want to save it to a jpeg file.
+     * @return the image file name.
+     */
+    public String getImageFileName()
+    {
+	return img;
+    }
+
+    /**
+     * This method will get the string representation of the image that is in this component.
+     * @return the image.
+     */
+    public String getImageFile()
+    {
+	return imgFile;
+    }
+
+    /**
+     * This method will set the index value.
+     * (Precondition: (num > 0))
+     * @param num the new index value.
+     */
+    public void startContent (int num)
+    {
+	index = num;
+    }
+    
+    /**
+     * This method will add the UserComponent to the content array list.
+     * @param comp the content of the component.
+     */
+    public void saveContent (UserComponent comp)
+    {
+	content.add(comp);
+    }
+
+    /**
+     * This method will set the data member value to the text passed
+     * to this method depending on the index value.
+     * @param text the value to be set to the data member.
+     */
+    public void saveContent (String text)
+    {
+	if (text == null)
+	    text = " ";
+
+	switch (index)
+	    {
+	    case 1 : type = text; break;
+	    case 2 : title = text; break;
+	    case 3 : content.add(text); break;
+	    case 4 : selected = text;break;
+            case 5 : visible = text;break;
+	    case 6 : image = text; break;
+	    case 7 : tooltip = text;break;
+            }
+    }
+
+    /**
+     * This method will encode the ImageIcon into its String representation using Based64encoder and
+     * it will also set the image's filename,height and width.
+     * @param image is the image to be converted.
+     * @param filename is the file name of the image.
+     */
+    public void saveImage (ImageIcon image,String filename) 
+    {
+	/*int w;
+	w = image.getIconWidth();
+	int h;
+	h = image.getIconHeight();
+
+	height = "" + h;
+	width = "" + w;
+	img = filename;
+	
+	if (w > 50)
+	    w = w - 50;
+	
+	if (h > 50)
+	    h = h - 50;
+	
+	ImageIcon icon;
+	icon = new ImageIcon(image.getImage().getScaledInstance(w,h,Image.SCALE_SMOOTH));
+   
+	if ( w < 0 || h < 0)
+	    {
+		System.out.println(img);
+		return;
+	    }
+
+	BufferedImage bi;
+	bi =new BufferedImage(w,h, BufferedImage.TYPE_INT_RGB);
+	Graphics2D big;
+	big = bi.createGraphics();
+	
+	big.drawImage(icon.getImage(),0,0,null);
+	
+	try	
+	    {
+		ByteArrayOutputStream stream;
+		stream = new ByteArrayOutputStream();
+		ImageIO.write(bi,"jpeg",stream);
+		byte[] bytes;
+		bytes = stream.toByteArray();
+		imgFile = Base64.encodeBytes(bytes);
+	    }
+	catch (IOException exp) {}
+	
+	big.dispose();
+	image = null;
+	bi = null;
+	icon = null;*/
+    }
+
+    /**
+     * This method will encode the Icon into its String representation using Based64encoder and
+     * it will also set the image's filename,height and width.
+     * @param image is the image to be converted.
+     * @param filename is the file name of the image
+     */
+    public void saveImage (Icon image,String filename)
+    {
+	if (image == null)
+	    System.out.println("null image??");
+	else if (image instanceof ImageIcon)
+	    saveImage((ImageIcon)image, filename);
+	else
+	    System.out.println("Its a icon");
+	    //saveIcon (image,filename);
+    }
+
+    /**
+     * This method will encode the Icon into its String representation using Based64encoder and
+     * it will also set the image's filename,height and width.
+     * @param image is the image to be converted.
+     * @param filename is the file name of the image.
+     */
+    private void saveIcon (Icon image,String filename)
+    {
+	JFrame lbl = new JFrame();
+	lbl.setVisible(false);
+
+	ImageIcon icon;
+	Image img;
+	img = iconToImage(image,lbl);
+	icon = new ImageIcon(img);
+	lbl.dispose();
+	lbl = null;
+	saveImage(icon,filename);
+    }
+
+    /**
+     * This method will save to content of this UserComponent to an xml file.
+     * The xml file should already have an opening tag and closing tag first before calling
+     * this method or it wont make a valid xml file.
+     * @param sx this is the xml file where we want to save it.
+     */
+    public void sendingXML (SaveToXML sx)
+    {
+	if (type != null)
+	    {
+		sx.startContent(1);
+		sx.saveContent(type);
+		sx.closeContent(1);
+	    }
+
+	if (title != null)
+	    {
+		sx.startContent(2);
+		sx.saveContent(title);
+		sx.closeContent(2);
+	    }
+
+	if (content != null)
+	    {
+		sx.startContent(3);
+		for (int j = 0 ; j < content.size() ; j++)
+		    {
+			if (content.get(j) instanceof String)
+			    sx.saveContent((String) content.get(j));
+			else
+			    {
+				sx.startContent(0);
+				UserComponent cmp;
+				cmp = (UserComponent) content.get(j);
+				cmp.sendingXML(sx);
+				sx.closeContent(0);
+			    }
+		    }
+		sx.closeContent(3);
+	    }
+
+	if (selected != null)
+	    {
+		sx.startContent(4);
+		sx.saveContent(selected);
+		sx.closeContent(4);
+	    }
+
+	if (visible != null)
+	    {
+		sx.startContent(5);
+		sx.saveContent(visible);
+		sx.closeContent(5);
+	    }
+
+	if (img != null)
+	    {
+		sx.startContent(6);
+		sx.saveImage(imgFile,img,width,height);
+		sx.closeContent(6);
+	    }
+	else
+	    {
+		if (image != null)
+		    {
+			sx.startContent(11);
+			sx.saveContent(image);
+			sx.closeContent(11);
+		    }
+	    }
+
+	if (tooltip != null)
+	    {
+		sx.startContent(12);
+		sx.saveContent(tooltip);
+		sx.closeContent(12);
+	    }
+    }
+
+    /**
+     * This method will convert an Icon to a BufferedImage.
+     * @param icon icon to be converted.
+     * @param targetComponent is the component where we want to draw this BufferedImage.
+     * @return is the BufferedImage of the icon.
+     */
+    public BufferedImage iconToImage(Icon icon, Component targetComponent) 
+    {
+	int w,h;
+	w = icon.getIconWidth();
+	h = icon.getIconHeight();
+	GraphicsConfiguration gc;
+	gc = targetComponent.getGraphicsConfiguration();
+	BufferedImage image;
+	image = gc.createCompatibleImage(w, h,Transparency.TRANSLUCENT);
+	
+	Graphics2D g;
+	g = image.createGraphics();
+	icon.paintIcon(targetComponent,g,0,0);
+	g.dispose();
+	return image;
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ZipFile.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ZipFile.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/feedback/ZipFile.java	(revision 31635)
@@ -0,0 +1,266 @@
+package org.greenstone.gatherer.feedback;
+
+import java.io.*;
+import java.util.jar.*;
+import java.util.zip.*;
+
+/**
+ * This class will help to zip and unzip files.
+ * @author Veronica Liesaputra
+ */
+public class ZipFile 
+{
+    /**
+     * This is the specified buffer size.
+     */
+    private static final int BUFFER = 2048;
+    
+    public ZipFile ()
+    {}
+
+    /**
+     * This method will unzip the MyZipFile.zip file and delete the file.
+     */
+    public void unZipFile ()
+    {
+	try
+	    {
+		BufferedOutputStream dest;
+		dest = null;
+		FileInputStream fis;
+		fis = new FileInputStream("MyZipFile.zip");
+		ZipInputStream zis;
+		zis = new ZipInputStream(new BufferedInputStream(fis));
+		ZipEntry entry;
+		while((entry = zis.getNextEntry()) != null) 
+		    {
+			int count;
+			byte data[];
+			data = new byte[BUFFER];
+			// write the files to the disk
+			File f;
+			f = new File(entry.getName());
+			if (isRightLogExtension(f) == true)
+			    {
+				FileOutputStream fos;
+				fos = new FileOutputStream(entry.getName());
+				dest = new BufferedOutputStream(fos, BUFFER);
+				while ((count = zis.read(data, 0, BUFFER)) != -1) 
+				    {
+					dest.write(data, 0, count);
+				    }
+				dest.flush();
+				dest.close();
+			    }
+		    }
+		zis.close();
+		File f3;
+		f3 = new File("MyZipFile.zip");
+		f3.delete();
+	    } 
+	catch (FileNotFoundException fexp) {}
+	catch(Exception e) 
+	    {
+		e.printStackTrace();
+	    }
+    } 
+
+    /**
+     * This method will zip all the xml files into one Jar file.
+     */
+    public void sendZipXMLFile ()
+     {
+	try 
+	    {
+		String dirname;
+		dirname = "xmlfeedback/";
+		BufferedInputStream origin;
+		origin = null;
+		FileOutputStream dest;
+		dest = new FileOutputStream(dirname + "Jar" + 
+					    FeedbackInterface.getCode() + 
+					    "File.jar");
+		JarOutputStream out;
+		out = new JarOutputStream(new BufferedOutputStream(dest));
+		out.setMethod(ZipOutputStream.DEFLATED);
+		byte data[];
+		data = new byte[BUFFER];
+		
+		File f;
+		f = new File(dirname + ".");
+		File[] fileList;
+		fileList = f.listFiles();
+		File[] files;
+		files = new File[fileList.length + 8];
+		
+		int index,j,i;
+		index = 0;
+		for (j = 0 ; j < fileList.length ; j++)
+		    {
+			String name;
+			name = fileList[j].getName();
+			
+			if (isRightXMLExtension(fileList[j]) == true)
+			    {
+				files[index] = fileList[j];
+				index++;
+			    }
+		    }
+		
+		for ( i = 0; i < index; i++) 
+		    {
+			FileInputStream fi;
+			fi = new FileInputStream(files[i]);
+			origin = new BufferedInputStream(fi, BUFFER);
+			ZipEntry entry;
+			entry = new ZipEntry(files[i].getName());
+			out.putNextEntry(entry);
+			int count;
+			while((count = origin.read(data, 0,BUFFER)) != -1) 
+			    {
+				out.write(data, 0, count);
+			    }
+			origin.close();
+		    }	
+
+		out.close();
+	    } 
+	catch(Exception e) 
+	    {
+		e.printStackTrace();
+	    }
+    }
+
+    /**
+     * This method will zip all .log file to MyZipFile.zip file and it will
+     * delete the history.log file.
+     */
+    public void sendZipFile ()
+    {
+	try 
+	    {
+		BufferedInputStream origin;
+		origin = null;
+		FileOutputStream dest;
+		dest = new FileOutputStream("MyZipFile.zip");
+		ZipOutputStream out;
+		out = new ZipOutputStream(new BufferedOutputStream(dest));
+		out.setMethod(ZipOutputStream.DEFLATED);
+		byte data[];
+		data = new byte[BUFFER];
+		
+		File f;
+		f = new File(".");
+		File[] fileList;
+		fileList = f.listFiles();
+		File[] files;
+		files = new File[fileList.length];
+		
+		int index,i,j;
+		index = 0;
+		for ( j = 0 ; j < fileList.length ; j++)
+		    {
+			if (isRightLogExtension(fileList[j]) == true)
+			    {
+				files[index] = fileList[j];
+				index++;
+			    }
+		    }
+		
+		for (i = 0; i < index; i++) 
+		    {
+			FileInputStream fi;
+			fi = new FileInputStream(files[i]);
+			origin = new BufferedInputStream(fi, BUFFER);
+			ZipEntry entry;
+			entry = new ZipEntry(files[i].getName());
+			out.putNextEntry(entry);
+			int count;
+			while((count = origin.read(data, 0,BUFFER)) != -1) 
+			    {
+				out.write(data, 0, count);
+			    }
+			origin.close();
+		    }
+		out.close();
+		File f2;
+		f2 = new File("history.log");
+		f2.delete();
+	    } 
+	catch(Exception e) 
+	    {
+		e.printStackTrace();
+	    }
+    }
+    
+    /**
+     * This method will get the extension of the given file.
+     * @param f the file we want to know the extension.
+     * @return  the file extension.
+     */
+    public String getExtension(File f)
+    {
+        String ext;
+	ext = null;
+        String s;
+	s = f.getName();
+        int i;
+	i = s.lastIndexOf('.');
+	
+        if (i > 0 &&  i < s.length() - 1)
+	    {
+		ext = s.substring(i+1).toLowerCase();
+	    }
+        return ext;
+    }
+   
+    /**
+     * This method will check whether or not the give file has
+     * an .xml extension.
+     * @param f the file we want to check the extension.
+     * @return  whether or not the file has xml as its file extension.
+     */
+    public boolean isRightXMLExtension(File f)
+    {
+	if (f.isDirectory() == true)
+	    {
+		return false;
+	    }
+	String ext;
+	ext = getExtension(f);
+	if ((ext != null) && (ext.compareTo("xml") == 0))
+	    return true;
+	else
+	    return false;
+    }
+
+    /**
+     * This method will check whether or not the give file has
+     * an .log extension.
+     * @param f the file we want to check the extension.
+     * @return  whether or not the file has log as its file extension.
+     */
+    public boolean isRightLogExtension(File f)
+    {
+	if (f.isDirectory() == true)
+	    return false;
+	
+	String ext;
+	ext = getExtension(f);
+	
+	if ((ext != null) && (ext.compareTo("log") == 0))
+	    return true;
+	else
+	    return false;
+    }
+}
+
+
+
+
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileAlreadyExistsException.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileAlreadyExistsException.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileAlreadyExistsException.java	(revision 31635)
@@ -0,0 +1,11 @@
+package org.greenstone.gatherer.file;
+
+import java.lang.Exception;
+
+public class FileAlreadyExistsException
+    extends Exception {
+
+    public FileAlreadyExistsException() {
+	super();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileAssociationManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileAssociationManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileAssociationManager.java	(revision 31635)
@@ -0,0 +1,367 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.file;
+
+import java.io.*;
+import java.util.regex.*;
+import javax.swing.table.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.gui.FileAssociationDialog;
+import org.greenstone.gatherer.gui.PreviewCommandDialog;
+import org.greenstone.gatherer.util.ExternalProgram;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+public class FileAssociationManager 
+    extends AbstractTableModel {
+    static final public String FILENAME_ARG = "%1";
+    static final private String ESCAPE = "\\\\"; // '\'
+    static final private String ESCAPED_ESCAPE = "\\\\\\\\"; // '\\'
+    static final private String SPACE = " ";
+    static final private String ESCAPED_SPACE = "\\\\ ";
+    private Element associations_element;
+    private File data_file;   
+
+    public FileAssociationManager() {
+	// Retrieve the associations_element from the config
+	associations_element = Configuration.getFileAssociations();
+	// Initialize the associations. This involves looking through all current associations searching for those with a command of "".
+	if(associations_element != null) {
+	    NodeList entries = associations_element.getElementsByTagName(StaticStrings.ENTRY_ELEMENT);
+	    for(int i = 0; i < entries.getLength(); i++) {
+		Element entry = (Element) entries.item(i);
+		String command = XMLTools.getValue(entry);
+		// If we encounter a command of ""...
+		if(command.length() == 0) {
+		    // if we are on windows, we default to the start command
+		    if(Utility.isWindows()) {
+			if (Utility.isWindows9x()) {
+			    XMLTools.setValue(entry, StaticStrings.WIN_9X_OPEN_COMMAND);
+			} else {
+			    XMLTools.setValue(entry, StaticStrings.WIN_OPEN_COMMAND);
+			}
+		    }
+		    // and if we are on mac, we default to the open program
+		    else if(Utility.isMac()) {
+			XMLTools.setValue(entry, StaticStrings.MAC_OPEN_COMMAND);
+		    }
+			//else { XMLTools.setValue(entry, StaticStrings.LINUX_OPEN_COMMAND); } // assume linux?
+		}
+		command = null;
+		entry = null;
+	    }
+	    entries = null;
+	}
+	else {
+	    DebugStream.println("Didn't parse anything. About to crash.");
+	}
+    }
+    
+    public void edit() {
+	FileAssociationDialog dialog = new FileAssociationDialog(this);
+	dialog.display(null);
+	dialog = null;		  
+    }
+
+    public String getBrowserCommand(String url) {
+	DebugStream.println("Get browser command: " + url);
+	// First off we try to retrieve one from the configuration
+	String command = Configuration.getPreviewCommand();
+	// If that worked, substitute in the url and return
+	if(command != null && command.length() > 0) {
+	    command = command.replaceAll("%1", url);
+	    DebugStream.println("Result = " + command);
+	    return command;
+	}
+	command = null;
+	// Failing that we have a guess at a sensible default 
+	if(Utility.isWindows()) {
+	    // we use cmd and start
+	    if (Utility.isWindows9x()) {
+		command = StaticStrings.WIN_9X_OPEN_COMMAND;//"command.com /c start \""+url+"\"";
+	    } else {
+		command = StaticStrings.WIN_OPEN_COMMAND;//"cmd.exe /c start \"\" \""+url+"\"";
+	    }
+	} else if (Utility.isMac()) {
+	    command = StaticStrings.MAC_OPEN_COMMAND; // "open %1"
+	} else {
+		// we try to look for a browser
+	    String [] browsers = new String [] {"mozilla", "netscape"};
+	    for (int i=0; i<browsers.length; i++) {
+		if (isAvailable(browsers[i])) {
+		    command = browsers[i]+ " %1";
+		    break;
+		}
+	    }
+		//if (command == null) { command = StaticStrings.LINUX_OPEN_COMMAND; } // "xdg-open %1"
+	}
+
+	// if we still haven't found something, prompt the user
+	if (command == null) {
+	    PreviewCommandDialog dialog = new PreviewCommandDialog();
+	    command = dialog.display();
+	    dialog.dispose();
+	    dialog = null;
+	}
+
+	// Store the result if any
+	if(command != null && !command.equals("")) {
+	    Configuration.setPreviewCommand(command);
+	    command = command.replaceAll(FILENAME_ARG, url);
+	    DebugStream.println("Result = " + command);
+	    return command;
+	}
+	// if we haven't got a command by now, we'll never get one
+	DebugStream.println("Result = null");
+	return null;
+	
+    }
+
+    public int getColumnCount() {
+	return 2;
+    }
+
+    public String getColumnName(int column) {
+	String name;
+	switch(column) {
+	case 0:
+	    name = Dictionary.get("FileAssociationDialog.Table.Extension");
+	    break;
+	default:
+	    name = Dictionary.get("FileAssociationDialog.Table.Command");
+	}
+	return name;
+    }
+   
+    public String [] getCommand(File file) {	
+	String command = null;
+	String [] commands = null;
+	if(file.isFile()) {
+	    // Determine extension
+	    String filename = file.getAbsolutePath();
+	    String extension = filename.substring(filename.lastIndexOf(".") + 1);
+	    // Try to retrieve a value from cache
+	    Element entry = getCommand(extension);
+	    if(entry != null) {
+		///ystem.err.println("Retrieved Value From Cache");
+		command = XMLTools.getValue(entry); 
+	    }
+	    if(command == null || command.length() == 0) {
+		///ystem.err.println("No Existing Command");
+		// If command is null, and we are on windows try searching the registry.
+		if(Utility.isWindows()) {
+		    //try the start command
+		    if (Utility.isWindows9x()) {
+			command = StaticStrings.WIN_9X_OPEN_COMMAND;
+		    } else {
+			command = StaticStrings.WIN_OPEN_COMMAND;
+		    }
+		    
+		}
+		
+		// If we are on a mac, default to using the open program
+		else if(Utility.isMac()) {
+		    command = StaticStrings.MAC_OPEN_COMMAND;
+		}
+		
+		//else { command = StaticStrings.LINUX_OPEN_COMMAND; } // If linux, default to using xdg-open
+		
+		// Otherwise display the dialog and ask the user to enter launching command.
+		if(command == null || command.length() == 0) {
+		    ///ystem.err.println("Show Dialog");
+		    // Show the dialog which forces a user to select the launch command for a certain file.
+		    FileAssociationDialog dialog = new FileAssociationDialog(this);
+		    command = dialog.display(extension);
+		    dialog = null;
+		}
+
+		// Hopefully by now we have a command, or else we're never going to get one. Add the association.
+		if (command != null && !command.equals("")) {
+		    // If no previous entry existed create one.
+		    if(entry == null) {
+			entry = associations_element.getOwnerDocument().createElement(StaticStrings.ENTRY_ELEMENT);
+			entry.setAttribute(StaticStrings.EXTENSION_ATTRIBUTE, extension);
+			associations_element.appendChild(entry);
+		    }
+		    // Replace the text in this node. Remember to replace the dummy filename with %1 - I dont think the filename will ever be in the comand now
+		    //XMLTools.setValue(entry, command.replaceAll(filename, FILENAME_ARG));
+		    XMLTools.setValue(entry, command);
+		}
+	    }
+
+	    if (command != null && !command.equals("")) {		
+		
+		// Make the command into a string []		
+		commands = command.split(" ");
+
+		// Now substitute any occurrences of %1 with its filename
+		// Note this is done after the split on spaces to avoid
+		// any conflict with filenames with spaces in them.
+
+		// We have to fix filename under windows to escape the backslashes
+		filename = filename.replaceAll(ESCAPE, ESCAPED_ESCAPE);
+		
+		// dealing with spaces in filepath when using start command
+		if(Utility.isWindows() && filename.indexOf(" ") != -1 && command.indexOf("start") != -1) { 
+			// On Windows, start command used spaces in filepath. In this case:
+			// start and its arguments all together need to go into one element of the commands array
+			// otherwise <cmd /c start "window title" "%1"> does not work if there are spaces in the
+			// file path %1, when running the Process with a command array.
+			
+			// Need <"start \"window\" \"%1\""> to be an element in the command array:
+			String[] tmp = commands;
+			int index = 0;
+			
+			for(int i = 0; i < commands.length; i++) {
+				if(commands[i].indexOf("start") != -1) {
+					index = i;
+				}
+			}
+			
+			commands = new String[index+1];
+			for(int i = 0; i < index; i++) {
+				commands[i] = tmp[i];
+			}
+			
+			commands[index] = tmp[index];
+			for(int i = index+1; i < tmp.length; i++) {
+				commands[index] = commands[index] + " " + tmp[i];
+			}
+			
+		}
+
+		for (int i=0; i<commands.length; i++) {
+		    // Replace %1 with the appropriate filename
+		    commands[i] = commands[i].replaceAll(FILENAME_ARG, filename);
+		}
+	    }
+	    
+	    entry = null;
+	    extension = null;
+	    filename = null;
+	}
+	return commands;
+    }
+
+    public Element getCommand(String target_extension) {
+	NodeList entries = associations_element.getElementsByTagName(StaticStrings.ENTRY_ELEMENT);
+	for(int i = 0; i < entries.getLength(); i++) { 
+	    Element entry = (Element) entries.item(i);
+	    String extension = entry.getAttribute(StaticStrings.EXTENSION_ATTRIBUTE);
+	    if(extension.equalsIgnoreCase(target_extension)) {
+		entries = null;
+		extension = null;
+		return entry;
+	    }
+	}
+	entries = null;
+	return null;
+    }
+
+    public String getCommandString(String target_extension) {
+	Element entry = getCommand(target_extension);
+	if(entry != null) {
+	    return XMLTools.getValue(entry);
+	}
+	else {
+	    return "";
+	}
+    }
+
+    public String getExtension(int index) {
+	NodeList entries = associations_element.getElementsByTagName(StaticStrings.ENTRY_ELEMENT);
+	if(0 <= index && index < entries.getLength()) {
+	    Element entry = (Element) entries.item(index);
+	    return entry.getAttribute(StaticStrings.EXTENSION_ATTRIBUTE);
+	}
+	return "";
+    }
+
+    public int getRowCount() {
+	return size();
+    }
+
+    public Object getValueAt(int row, int column) {
+	String extension = getExtension(row);
+	switch(column) {
+	case 0:
+	    return extension;
+	default:
+	    return getCommandString(extension);
+	}
+    }
+
+    public void save() {
+    }
+
+    public void setCommand(String extension, String command) {
+	DebugStream.println("Set Launch: " + extension + " with " + command);
+	// Retrieve any existing entry for this extension
+	Element entry = getCommand(extension);
+	// If no previous entry existed create one.
+	if(entry == null && command != null) {
+	    entry = associations_element.getOwnerDocument().createElement(StaticStrings.ENTRY_ELEMENT);
+	    entry.setAttribute(StaticStrings.EXTENSION_ATTRIBUTE, extension);
+	    associations_element.appendChild(entry);
+	}
+
+	if(command != null) {
+	    // Replace the text in this node. If the user has used filename instead of %1 then too bad.
+	    XMLTools.setValue(entry, command);
+	}
+	else {
+	    // Remove the entry
+	    associations_element.removeChild(entry);
+	}
+	entry = null;
+	fireTableDataChanged(); // Can't be anymore efficient as DOM does not gareuntee ordering of new child nodes is consistant
+    }
+
+    public int size() {
+	NodeList entries = associations_element.getElementsByTagName(StaticStrings.ENTRY_ELEMENT);
+	return entries.getLength();
+    }
+
+
+    protected boolean isAvailable(String program) {
+	try {
+	    ExternalProgram e = new ExternalProgram("which", program);
+	    e.exitProgram();
+	    String out = e.getLineOfProgramOutput();
+	    if (out == null) {
+		return false;
+	    }
+	    return true;
+	} catch (Exception exc) {
+	    return false;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileFilter.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileFilter.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileFilter.java	(revision 31635)
@@ -0,0 +1,10 @@
+package org.greenstone.gatherer.file;
+
+public class FileFilter {
+    public boolean exclude;
+    public String filter;
+    public FileFilter(String filter, boolean exclude) {
+	this.exclude = exclude;
+	this.filter = filter;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileJob.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileJob.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileJob.java	(revision 31635)
@@ -0,0 +1,168 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.file;
+
+import javax.swing.tree.*;
+import org.greenstone.gatherer.util.DragComponent;
+
+/** This object encapsulates all the information about a certain file movement job. This job be to create a copy of a file from one place to another, or to simply delete a file. 
+ * @author John Thompson
+ * @version 2.3c
+ */
+public class FileJob {
+    /** true to mark that this file has already been copied, false otherwise. */
+    public boolean done = false;
+
+    /** The type of this movement as an byte. */
+    public byte type = 0;
+    /** The DragComponent source of this file, most likely a GTree. */
+    public DragComponent source       = null;
+    /** The DragComponent to move the file to, again most likely a GTree. */
+    public DragComponent target       = null;
+    /** The unique identifier shared by all jobs created by the same action. */
+    private long id = 0;
+    /** The path to the destination node as a string. Used because any reference based path or variables quickly becomes obsolete. */
+    private TreePath destination_path = null;
+    /** The path to the origin node as a string. Used because any reference based path or variables quickly becomes obsolete. */
+    private TreePath origin_path = null;
+    /** An element of the job type enumeration indicating a copy action. */
+    static final public byte COPY   = 1;
+    /** An element of the job type enumeration indicating a delete action. */
+    static final public byte DELETE = 2;
+    /** An element of the job type enumeration indicating a move action. */
+    static final public byte MOVE   = 3;
+    /** An element of the job type enumeration indicating a new folder action. */
+    static final public byte NEW_FOLDER = 4;
+    /** An element of the job type enumeration indicating an empty folder delete action. */
+    static final public byte DELETE_EMPTY_DIRECTORY = 5;
+    /** An element of the job type enumeration indicating a rename action. */
+    static final public byte RENAME = 6;
+    /** An element of the job type enumeration indicating a replace action. */
+    static final public byte REPLACE = 7;
+    /** An element of the job type enumeration indicating a copy action (but don't look for metadata. */
+    static final public byte COPY_FILE_ONLY   = 8;
+
+    /** Constructor.
+     * @param id A unique identifier for this job (and others created with a single gesture) as a long.
+     * @param source The DragComponent source of this file, most likely a GTree.
+     * @param orig The FileNode you wish to mode.
+     * @param target The DragComponent to move the file to, again most likely a GTree.
+     * @param dest The files new FileNode parent within the target.
+     * @param type The type of this movement as an int, either COPY or DELETE.
+     */
+    public FileJob(long id, DragComponent source, FileNode orig, DragComponent target, FileNode dest, byte type) {
+	this.id = id;
+	this.source = source;
+	this.target = target;
+	this.type = type;
+	///ystem.err.println("New Job: " + type + ", " + source + ", " + target);
+	// Dont store FileNodes which can go stale. Store paths instead, which are used to locate current 'fresh' versions of nodes.
+	if(dest != null) {
+	    this.destination_path = new TreePath(dest.getPath());
+	    ///ystem.err.println("Destination Path: " + destination_path);
+	}
+	if(orig != null) {
+	    this.origin_path = new TreePath(orig.getPath());
+	    ///atherer.println("Origin Path: " + origin_path);
+	}
+    }
+
+    /** Retrieve the destination node. Watch out for stale versions by always attempting to load the node at destination_path first. */
+    public FileNode getDestination() {
+	FileNode destination = null;
+	if(destination_path != null) {
+	    if(target != null) {
+		FileSystemModel model = (FileSystemModel)target.getTreeModel();
+		destination = model.getNode(destination_path);
+	    }
+	    // If the above fails, a stale copy may be better than nothing.
+	    else {
+		destination = (FileNode) destination_path.getLastPathComponent();
+	    }
+	}
+	return destination;
+    }
+
+    /** Retrieve the origin node. Watch out for stale versions by always attempting to load the node at origin_path first. */
+    public FileNode getOrigin() {
+	FileNode origin = null;
+	if(origin_path != null) {
+	    if(source != null) {
+		FileSystemModel model = (FileSystemModel)source.getTreeModel();
+		origin = model.getNode(origin_path);
+	    }
+	    // If the above fails, a stale copy may be better than nothing.
+	    else {
+		origin = (FileNode) origin_path.getLastPathComponent();
+	    }
+	}
+	return origin;
+    }
+
+    /** Retrieve the id for this job. */
+    public long ID() {
+	return id;
+    }
+
+    public String toString() {
+	StringBuffer text = new StringBuffer("");
+	switch(type) {
+	case COPY:
+	    text.append("copy ");
+	    break;
+	case DELETE:
+	    text.append("delete ");
+	    break;
+	case MOVE:
+	    text.append("move ");
+	    break;
+	case DELETE_EMPTY_DIRECTORY:
+	    text.append("empty directory delete ");
+	    break;
+	default:
+	    text.append("unknown ");
+	}
+	FileNode origin = getOrigin();
+	if(origin != null) {
+	    text.append(origin.getFile().getAbsolutePath());
+	}
+	else {
+	    text.append("ERROR!");
+	}
+	if (type != DELETE && type != DELETE_EMPTY_DIRECTORY) {
+	    text.append(" -> ");
+	    FileNode destination = getDestination();
+	    if(destination != null) {
+		text.append(destination.getFile().getAbsolutePath());
+	    }  
+	    else {
+		text.append("Recycle Bin");
+	    }
+	}
+	return text.toString();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileManager.java	(revision 31635)
@@ -0,0 +1,561 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, NZDL Project, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.file;
+
+import java.io.File;
+import javax.swing.*;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.collection.CollectionTree;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.gui.ExplodeMetadataDatabasePrompt;
+import org.greenstone.gatherer.gui.ReplaceSrcDocWithHtmlPrompt; 
+import org.greenstone.gatherer.gui.GProgressBar;
+import org.greenstone.gatherer.gui.NewFolderOrFilePrompt;
+import org.greenstone.gatherer.gui.RenamePrompt;
+import org.greenstone.gatherer.gui.tree.DragTree;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.DragComponent;
+import org.greenstone.gatherer.util.Utility;
+
+/** Manages the moving of files within a separate thread.
+ * @author John Thompson, NZDL Project, University of Waikato
+ */
+public class FileManager
+{
+    /** Not only the queue of files to be moved, but also the object that moves them. */
+    static private FileQueue file_queue = null;
+
+    public static final int COPY = 0;
+    public static final int MOVE = 1;
+
+    public static final int FILE_TYPE = 0;
+    public static final int FOLDER_TYPE = 1;
+    protected static File startup_directory = null;
+
+
+    /** Constructor.
+     * @see org.greenstone.gatherer.file.FileQueue
+     */
+    public FileManager()
+    {
+	file_queue = new FileQueue();
+	file_queue.start();
+    }
+
+
+    /** Determine what action should be carried out by the file queue, and add all of the necessary file jobs. */
+    public void action(DragComponent source, FileNode[] source_nodes, DragComponent target, FileNode target_node)
+    {
+	// Check there is something to do
+	if (source_nodes == null || source_nodes.length == 0) {
+	    return;
+	}
+
+	// We need a unique ID for each file task
+	long id = System.currentTimeMillis();
+
+	// If source and target are the same we're moving
+	if (source == target) {
+	    // Start a new move FileTask and we're done
+	    (new FileTask(id, source, source_nodes, target, target_node, FileJob.MOVE)).start();
+	    return;
+	}
+
+	// If target isn't the RecycleBin, we're copying
+	if (!(target instanceof RecycleBin)) {
+	    // Start a new copy FileTask and we're done
+	    (new FileTask(id, source, source_nodes, target, target_node, FileJob.COPY)).start();
+	    return;
+	}
+
+	// We're deleting... but first make sure source isn't read-only
+	boolean read_only_source = false;
+
+	// The workspace tree is read-only...
+	if (source.toString().equals("Workspace")) {
+	    read_only_source = true;
+
+	    // ...except for files from the "Downloaded Files" folder
+	    String downloaded_files_folder_path = Gatherer.getGLIUserCacheDirectoryPath();
+	    for (int i = 0; i < source_nodes.length; i++) {
+		// Is this the "Downloaded Files" folder?
+		if (source_nodes[i].getFile().getAbsolutePath().startsWith(downloaded_files_folder_path)) {
+		    read_only_source = false;
+		}
+	    }
+	}
+
+	// The source is read-only, so tell the user and abort
+	if (read_only_source) {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Read_Only"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	    return;
+	}
+
+	// Start a new delete FileTask and we're done
+	(new FileTask(id, source, source_nodes, target, target_node, FileJob.DELETE)).start();
+    }
+
+    /** For moving and copying of folders. */
+    public void action(File sourceFolder, File targetFolder, int operation) {
+	(new SimpleFileTask(sourceFolder, targetFolder, operation)).start();
+    }
+    
+
+    /** Retrieves the file queue object. */
+    public FileQueue getQueue()
+    {
+	return file_queue;
+    }
+
+    /** Performs the simple file task of moving or copying folders. */
+    private class SimpleFileTask
+	extends Thread
+    {
+	private File sourceFolder;
+	private File targetFolder;
+	int operation; // MOVE or COPY
+
+	public SimpleFileTask(File sourceFolder, File targetFolder, int operation)
+	{
+	    this.sourceFolder = sourceFolder;	    
+	    this.targetFolder = targetFolder;
+	    this.operation = operation;
+	}
+
+
+	public void run()
+	{
+	    // check if we're moving or overwriting the current collection
+	    String currentColPath = Gatherer.getCollectDirectoryPath()+CollectionManager.getLoadedCollectionName();
+	    if(currentColPath.equals(sourceFolder.getAbsolutePath()) 
+	       || currentColPath.equals(targetFolder.getAbsolutePath())) {
+		Gatherer.g_man.saveThenCloseCurrentCollection();
+	    }
+	    
+	    // if moving, try a simple move operation (if it works, it
+	    // shouldn't take long at all and doesn't need a progress bar)
+	    if(operation == MOVE && sourceFolder.renameTo(targetFolder)) {
+		//System.err.println("**** A simple renameTo() worked.");
+		WorkspaceTreeModel.refreshGreenstoneCollectionsNode();
+		return;
+	    }
+
+	    // Reset the progress bar and set it to indeterminate while calculating its size
+	    GProgressBar progress_bar = file_queue.getProgressBar();
+	    progress_bar.reset();
+	    progress_bar.setIndeterminate(true);
+
+	    String status = "FileActions.Moving";
+	    if(operation == COPY) {
+		status = "FileActions.Copying";	    
+	    }
+	    progress_bar.setString(Dictionary.get(status));
+	    file_queue.getFileStatus().setText(Dictionary.get(status,
+							      file_queue.formatPath(status,
+									 sourceFolder.getAbsolutePath(), 
+									 file_queue.getFileStatus().getSize().width)));
+
+	    // do the move or copy operation
+	    try {
+		//System.err.println("**** Copying " + sourceFolder + " to: " + targetFolder);
+		file_queue.copyDirectoryContents(sourceFolder, targetFolder);		
+	    } catch(Exception e) {
+		JOptionPane.showMessageDialog(Gatherer.g_man, e.getMessage(), 
+					      "Can't perform file operation", JOptionPane.ERROR_MESSAGE);
+
+		progress_bar.setIndeterminate(false);
+		progress_bar.clear();
+		return;
+	    }
+		
+	    // if moving, delete the original source folder and
+	    // update the docs in GS collections node in the workspace tree
+	    
+	    if(operation == MOVE) {
+		Utility.delete(sourceFolder);
+		WorkspaceTreeModel.refreshGreenstoneCollectionsNode();
+	    }
+
+
+	    progress_bar.setIndeterminate(false);
+	    progress_bar.clear();
+	    file_queue.getFileStatus().setText(Dictionary.get("FileActions.No_Activity"));
+	    progress_bar.setString(Dictionary.get("FileActions.No_Activity"));
+	}
+    }
+
+    private class FileTask
+	extends Thread
+    {
+	private long id;
+	private DragComponent source;
+	private FileNode[] source_nodes;
+	private DragComponent target;
+	private FileNode target_node;
+	private byte type;
+
+
+	public FileTask(long id, DragComponent source, FileNode[] source_nodes, DragComponent target, FileNode target_node, byte type)
+	{
+	    this.id = id;
+	    this.source = source;
+	    this.source_nodes = source_nodes;
+	    this.target = target;
+	    this.target_node = target_node;
+	    this.type = type;
+	}
+
+
+	public void run()
+	{
+	    // Reset the progress bar and set it to indeterminate while calculating its size
+	    GProgressBar progress_bar = file_queue.getProgressBar();
+	    progress_bar.reset();
+	    progress_bar.setIndeterminate(true);
+
+	    // Calculate the progress bar size
+	    boolean cancelled = file_queue.calculateSize(source_nodes);
+	    if (!cancelled) {
+		file_queue.addJob(id, source, source_nodes, target, target_node, type);
+		if (Gatherer.isGsdlRemote) {
+		    String collection_name = CollectionManager.getLoadedCollectionName();
+
+		    // Perform the appropriate action based on the job type (RemoteGreenstoneServer will queue)
+		    if (type == FileJob.COPY) {
+			// Copies: upload all the files at once in one zip file
+			File[] source_files = new File[source_nodes.length];
+			for (int i = 0; i < source_nodes.length; i++) {
+			    source_files[i] = source_nodes[i].getFile();
+			}
+			Gatherer.remoteGreenstoneServer.uploadFilesIntoCollection(collection_name, source_files, target_node.getFile());
+		    }
+		    else if (type == FileJob.DELETE) {
+			// Deletes: delete each top-level file/directory one at a time
+			for (int i = 0; i < source_nodes.length; i++) {
+			    Gatherer.remoteGreenstoneServer.deleteCollectionFile(collection_name, source_nodes[i].getFile());
+			}
+		    }
+		    else if (type == FileJob.MOVE) {
+			// Moves: move each top-level file/directory one at a time
+			for (int i = 0; i < source_nodes.length; i++) {
+			    Gatherer.remoteGreenstoneServer.moveCollectionFile(
+                                     collection_name, source_nodes[i].getFile(), target_node.getFile());
+			}
+		    }
+		}
+	    }
+
+	    progress_bar.setIndeterminate(false);
+	    progress_bar.clear();
+	}
+    }
+
+    public void explodeMetadataDatabase(File file)
+    {
+	// This must go in a separate thread because we need the progress bar to work (remote Greenstone server)
+	new ExplodeMetadataDatabasePromptTask(file).start();
+    }
+
+    // Works with replace_srcdoc_with_html.pl
+    public void replaceSrcDocWithHtml(File[] files)
+    {
+	// This must go in a separate thread because we need the progress bar to work (remote Greenstone server)
+	new ReplaceSrcDocWithHtmlPromptTask(files).start();
+    }
+
+    private class ExplodeMetadataDatabasePromptTask
+	extends Thread
+    {
+	private File metadata_database_file = null;
+
+	public ExplodeMetadataDatabasePromptTask(File metadata_database_file)
+	{
+	    this.metadata_database_file = metadata_database_file;
+	}
+
+	public void run()
+	{
+	    ExplodeMetadataDatabasePrompt emp = new ExplodeMetadataDatabasePrompt(metadata_database_file);
+	}
+    }
+
+    // Works with replace_srcdoc_with_html.pl
+    private class ReplaceSrcDocWithHtmlPromptTask
+	extends Thread
+    {
+	private File[] replace_these_srcdoc_files = null;
+
+	public ReplaceSrcDocWithHtmlPromptTask(File[] replace_these_srcdoc_files)
+	{
+	    this.replace_these_srcdoc_files = replace_these_srcdoc_files;
+	}
+
+	public void run()
+	{
+	    ReplaceSrcDocWithHtmlPrompt prompt = new ReplaceSrcDocWithHtmlPrompt(replace_these_srcdoc_files);
+	}
+    }
+
+
+    public void openFileInExternalApplication(File file)
+    {
+	// This must go in a separate thread because we need the progress bar to work (remote Greenstone server)
+	new OpenFileInExternalApplicationTask(file).start();
+    }
+
+
+    private class OpenFileInExternalApplicationTask
+	extends Thread
+    {
+	private File file = null;
+
+	public OpenFileInExternalApplicationTask(File file)
+	{
+	    this.file = file;
+	}
+
+	public void run()
+	{
+	    // If we're using a remote Greenstone server, we need to download the file before viewing it...
+	    if (Gatherer.isGsdlRemote) {
+		// ... but only if it is inside the collection and we haven't already downloaded it
+		if (file.getAbsolutePath().startsWith(Gatherer.getCollectDirectoryPath()) && file.length() == 0) {
+		    if (Gatherer.remoteGreenstoneServer.downloadCollectionFile(
+                                 CollectionManager.getLoadedCollectionName(), file).equals("")) {
+			// Something has gone wrong downloading the file
+			return;
+		    }
+		}
+	    }
+
+	    // View the file in an external application
+	    Gatherer.spawnApplication(file);
+	}
+    }
+
+
+    public void newDummyDoc(DragTree tree, CollectionTreeNode parent_node){
+	newFolderOrDummyDoc(tree, parent_node, FILE_TYPE);
+    }
+
+
+    public void newFolder(DragTree tree, CollectionTreeNode parent_node) {
+	newFolderOrDummyDoc(tree, parent_node, FOLDER_TYPE);
+    }
+
+
+    protected void newFolderOrDummyDoc(DragTree tree, CollectionTreeNode parent_node, int type) {
+	(new NewFolderOrDummyDocumentTask(tree, parent_node, type)).start();
+    }
+
+
+    private class NewFolderOrDummyDocumentTask
+	extends Thread
+    {
+	private DragTree tree = null;
+	private CollectionTreeNode parent_node = null;
+	private int type;
+
+	public NewFolderOrDummyDocumentTask(DragTree tree, CollectionTreeNode parent_node, int type)
+	{
+	    this.tree = tree;
+	    this.parent_node = parent_node;
+	    this.type = type;
+	}
+
+	public void run()
+	{
+	    // Ask the user for the directories name.
+	    String extension = "";
+	    if (type == FILE_TYPE) {
+		extension = ".nul";
+	    }
+
+	    NewFolderOrFilePrompt new_folder_prompt = new NewFolderOrFilePrompt(parent_node, type, extension);
+	    String name = new_folder_prompt.display();
+	    new_folder_prompt.dispose();
+	    new_folder_prompt = null;
+
+	    // And if the name is non-null...
+	    if (name != null) {
+		FileSystemModel model = (FileSystemModel) tree.getModel();
+		File folder_file = new File(parent_node.getFile(), name);
+
+		//... check if it already exists.
+		if (folder_file.exists()) { 
+		    if (type == FILE_TYPE) {
+			JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Already_Exists_No_Create", name), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		    }
+		    else {
+			JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Folder_Already_Exists", name), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		    }
+		}
+		// Otherwise create it.
+		else {
+		    try {
+			if (type == FILE_TYPE) {
+			    folder_file.createNewFile();
+			    if (Gatherer.isGsdlRemote) {
+				Gatherer.remoteGreenstoneServer.uploadCollectionFile(
+                                         CollectionManager.getLoadedCollectionName(), folder_file);
+			    }
+			}
+			else {
+			    folder_file.mkdirs();
+			    if (Gatherer.isGsdlRemote) {
+				Gatherer.remoteGreenstoneServer.newCollectionDirectory(
+                                         CollectionManager.getLoadedCollectionName(), folder_file);
+			    }
+			}
+
+			// Update the parent node to show the new folder
+			parent_node.refresh();
+
+			// Refresh workspace tree (collection tree is done automatically)
+			Gatherer.g_man.refreshWorkspaceTree(DragTree.COLLECTION_CONTENTS_CHANGED);
+		    }
+		    catch (Exception exception) {
+			if (type == FILE_TYPE) {
+			    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Create_Error", name), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+			}
+			else {
+			    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Folder_Create_Error", name), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+			}
+		    }
+		}
+		
+		folder_file = null;
+		model = null;
+	    }
+	    name = null;
+	}
+    }
+
+
+    public void renameCollectionFile(CollectionTree collection_tree, CollectionTreeNode collection_tree_node)
+    {
+	// This must go in a separate thread because we need the progress bar to work (remote Greenstone server)
+	new RenameTask(collection_tree, collection_tree_node).start();
+    }
+
+
+    private class RenameTask
+	extends Thread
+    {
+	private CollectionTree collection_tree = null;
+	private CollectionTreeNode collection_tree_node = null;
+
+	public RenameTask(CollectionTree collection_tree, CollectionTreeNode collection_tree_node)
+	{
+	    this.collection_tree = collection_tree;
+	    this.collection_tree_node = collection_tree_node;
+	}
+
+	public void run()
+	{
+	    RenamePrompt rename_prompt = new RenamePrompt(collection_tree_node);
+	    String new_collection_file_name = rename_prompt.display();
+	    rename_prompt.dispose();
+	    rename_prompt = null;
+
+	    if (new_collection_file_name != null) {
+		File collection_file = collection_tree_node.getFile();
+		File new_collection_file = new File(collection_file.getParentFile(), new_collection_file_name);
+		CollectionTreeNode new_collection_tree_node = new CollectionTreeNode(new_collection_file);
+		file_queue.addJob(System.currentTimeMillis(), collection_tree, new FileNode[] { collection_tree_node }, collection_tree, new_collection_tree_node, FileJob.RENAME);
+		if (Gatherer.isGsdlRemote) {
+		    Gatherer.remoteGreenstoneServer.moveCollectionFile(
+                             CollectionManager.getLoadedCollectionName(), collection_file, new_collection_file);
+		}
+	    }
+	}
+    }
+
+    public void replaceCollectionFile(CollectionTree collection_tree, CollectionTreeNode collection_tree_node)
+    {
+	// This must go in a separate thread because we need the progress bar to work (remote Greenstone server)
+	new ReplaceTask(collection_tree, collection_tree_node).start();
+    }
+
+
+    private class ReplaceTask
+	extends Thread
+    {
+	private CollectionTree collection_tree = null;
+	private CollectionTreeNode collection_tree_node = null;
+
+	public ReplaceTask(CollectionTree collection_tree, CollectionTreeNode collection_tree_node)
+	{
+	    this.collection_tree = collection_tree;
+	    this.collection_tree_node = collection_tree_node;
+	}
+
+	public void run()
+	{
+	    JFileChooser file_chooser = new JFileChooser(startup_directory);
+	    file_chooser.setDialogTitle(Dictionary.get("ReplacePrompt.Title"));
+	    File new_file = null;
+	    int return_val = file_chooser.showOpenDialog(null);
+	    if(return_val == JFileChooser.APPROVE_OPTION) {
+		new_file = file_chooser.getSelectedFile();
+	    }
+	    
+	    if (new_file == null) {
+		return;
+	    }
+	    
+	    // save the search path for next time
+	    startup_directory = new_file.getParentFile();
+	    // make up a node for the file to bring in
+	    WorkspaceTreeNode source_node = new WorkspaceTreeNode(new_file);
+
+	    File target_directory = collection_tree_node.getFile().getParentFile();
+	    CollectionTreeNode new_collection_tree_node = new CollectionTreeNode(new File(target_directory, new_file.getName()));
+	    // copy the new file in - but don't bring metadata
+	    file_queue.addJob(System.currentTimeMillis(), Gatherer.g_man.gather_pane.workspace_tree, new FileNode[] { source_node }, collection_tree, (FileNode)collection_tree_node.getParent(),  FileJob.COPY_FILE_ONLY);
+	    if (Gatherer.isGsdlRemote) {
+		String collection_name = CollectionManager.getLoadedCollectionName();
+		Gatherer.remoteGreenstoneServer.deleteCollectionFile(collection_name, collection_tree_node.getFile());
+		Gatherer.remoteGreenstoneServer.uploadFilesIntoCollection(collection_name, new File[] { new_file }, target_directory);
+	    }
+	    // do a replace of old file with new file
+	    file_queue.addJob(System.currentTimeMillis(), collection_tree, new FileNode[] { collection_tree_node }, collection_tree, new_collection_tree_node, FileJob.REPLACE);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileNode.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileNode.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileNode.java	(revision 31635)
@@ -0,0 +1,393 @@
+package org.greenstone.gatherer.file;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import javax.swing.*;
+import javax.swing.filechooser.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.metadata.FilenameEncoding;
+import org.greenstone.gatherer.util.ArrayTools;
+
+
+public abstract class FileNode
+    implements MutableTreeNode
+{
+    protected boolean allows_children = true;
+    protected ArrayList child_nodes = null;
+    protected ArrayList child_nodes_unfiltered = null;
+    protected File file = null;
+    protected FileSystemModel model = null;
+    protected MutableTreeNode parent = null;	
+
+	protected String urlEncodedFileName = "";
+    protected String urlEncodedFilePath = "";
+	protected String filenameEncoding = "";
+    /** The string that is displayed as the filename. Attempts to be in the correct encoding. */
+    protected String displayFileName = null;
+	
+
+    public FileNode(File file)
+    {
+		this.file = file;
+
+		if (file != null) {
+			// Files cannot have children
+		    if(file.isFile()) {
+				// Cache this result to prevent unceasing missing disk messages being thrown if the
+				// removable media was, um, removed after directory mapped
+				this.allows_children = false;
+		    }
+		    filenameEncoding = "";
+		    urlEncodedFilePath = FilenameEncoding.calcURLEncodedFilePath(file);
+		    urlEncodedFileName = FilenameEncoding.calcURLEncodedFileName(urlEncodedFilePath);
+
+		    // work out the display string (extra special processing for CollectionTreeNodes)
+		    displayFileName = calcDisplayString();	    
+		}
+    }
+
+    public String getURLEncodedFileName() { return urlEncodedFileName; }
+
+    public String getURLEncodedFilePath() { return urlEncodedFilePath; }
+
+    public String getFilenameEncoding() { return filenameEncoding; }
+
+
+	/** This method returns a string representation of the filenodes in the tree, 
+	* that can then be displayed in the tree. Overridden in subclass CollectionTreeNode.
+	* Turn FilenameEncoding.DEBUGGING on to see URLEncoded filenames.
+	*/
+    protected String calcDisplayString() {	
+		if(FilenameEncoding.DEBUGGING) {
+			return getURLEncodedFileName();
+		} else {
+			return file.getName();
+		}
+    }
+
+    /** Returns the children of the node as an Enumeration. */
+    public Enumeration children()
+    {
+	return new FileEnumeration();
+    }
+
+
+    /** Returns true if the receiver allows children.  */
+    public boolean getAllowsChildren()
+    {
+	return allows_children;
+    }
+
+
+    /** Returns the child TreeNode at index childIndex. */
+    public TreeNode getChildAt(int index)
+    {
+	return (TreeNode) child_nodes.get(index);
+    }
+
+
+    /** Returns the number of children TreeNodes the receiver contains. */
+    public int getChildCount()
+    {
+	map();
+
+	// Use the number of (filtered) child nodes
+	if (child_nodes != null) {
+	    return child_nodes.size();
+	}
+
+	return 0;
+    }
+
+
+    /** Returns the index of node in the receivers children. */
+    public int getIndex(TreeNode node)
+    {
+	if (child_nodes != null) {
+	    return child_nodes.indexOf(node);
+	}
+
+	return -1;
+    }
+
+
+    /** Returns the parent TreeNode of the receiver. */
+    public TreeNode getParent()
+    {
+	return parent;
+    }
+
+
+    /** Adds child to the receiver at index. */
+    public void insert(MutableTreeNode child, int index)
+    {
+	DebugStream.println("Insert " + child + " in " + this + " at index " + index + " [Model: " + model + "]");
+	if (child == null) {
+	    return;
+	}
+
+	try {
+	    FileNode child_node = (FileNode) child;
+	    child_nodes.add(index, child_node);
+	    child_node.model = model;
+	    child_node.parent = this;
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+
+
+    /** Returns true if the receiver is a leaf. */
+    public boolean isLeaf()
+    {
+	return (allows_children == false);
+    }
+
+
+    /** Removes the child at index from the receiver. */
+    public void remove(int index)
+    {
+	if (index >= 0 && index < child_nodes.size()) {
+	    child_nodes.remove(index);
+	}
+    }
+
+
+    /** Removes node from the receiver. */
+    public void remove(MutableTreeNode node)
+    {
+	remove(getIndex(node));
+    }
+
+
+    /** Removes the receiver from its parent. */
+    public void removeFromParent()
+    {
+	parent.remove(this);
+	parent = null;
+    }
+
+
+    /** Resets the user object of the receiver to object. */
+    public void setUserObject(Object object) {
+	try {
+	    file = (File) object;
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+
+
+    // -------------------------------------------------------------------------------
+
+
+    public void add(MutableTreeNode child)
+    {
+	insert(child, child_nodes.size());
+    }
+
+
+    protected abstract FileNode addChildNode(File file);
+
+
+    /** Compare two FileNodes for equality. */
+    public boolean equals(FileNode node)
+    {
+	if (node == null) {
+	    // Definitely not a match
+	    return false;
+	}
+
+	if (file != null) {
+	    return file.equals(node.getFile());
+	}
+	else {
+	    return toString().equals(node.toString());
+	}
+    }
+
+
+    /** Retrieve the file node at the given index, regardless of filters set. */
+    public FileNode getChildAtUnfiltered(int index)
+    {
+ 	if (index >= 0 && index < size()) {
+ 	    return (FileNode) child_nodes_unfiltered.get(index);
+ 	}
+ 	return null;
+    }
+
+
+    public File getFile() {
+	return file;
+    }
+
+
+    /** Retrieves the tree path from the root node to this node. */
+    public TreeNode[] getPath() {
+	int count = 0;
+	TreeNode current = this;
+	while(current != null) {
+	    count++;
+	    current = current.getParent();
+	}
+	TreeNode[] path = new TreeNode[count];
+	current = this;
+	while(current != null) {
+	    path[count - 1] = current;
+	    count--;
+	    current = current.getParent();
+	}
+	return path;
+    }
+
+
+    public boolean isFileSystemRoot() {
+	if (file != null) {
+	    return FileSystemView.getFileSystemView().isFileSystemRoot(file);
+	}
+	else {
+	    return false;
+	}
+    }
+
+
+    /** Overridden if necessary by subclasses. */
+    public boolean isInLoadedCollection()
+    {
+	return false;
+    }
+
+
+    /** Overridden if necessary by subclasses. */
+    public boolean isReadOnly()
+    {
+	return false;
+    }
+
+
+    public void map()
+    {
+	// If this node has already been mapped, don't bother doing it again
+	if (child_nodes != null) {
+	    return;
+	}
+	child_nodes = new ArrayList();
+
+	// General case, only map if children are allowed
+	if (file != null && getAllowsChildren()) {
+	    File[] files = file.listFiles();
+	    if (files != null && files.length > 0) {
+		// Sort the child files
+		ArrayTools.sort(files);
+
+		// Now add them to child_nodes_unfiltered
+		child_nodes_unfiltered = new ArrayList();
+		for (int i = 0; i < files.length; i++) {
+		    FileNode child_node = this.addChildNode(files[i]);
+		    child_nodes_unfiltered.add(child_node);
+		}
+
+		// Apply the filters set in the model
+		FileFilter[] filters = model.getFilters();
+		for (int i = 0; filters != null && i < filters.length; i++) {
+		    files = ArrayTools.filter(files, filters[i].filter, filters[i].exclude);
+		}
+
+		// Add the files left after filtering to child_nodes
+		for (int i = 0, j = 0; (i < child_nodes_unfiltered.size() && j < files.length); i++) {
+		    // Use the FileNode object in child_nodes_unfiltered rather than creating another
+		    FileNode file_node = (FileNode) child_nodes_unfiltered.get(i);
+		    if (file_node.getFile().equals(files[j])) {
+			child_nodes.add(file_node);
+			j++;
+		    }
+		}
+		
+			// in case any filename encodings had gone stale, 
+			// (recalculate these and) refresh the display name
+			refreshDescendantEncodings();			
+					
+	    }
+
+	    model.nodeStructureChanged(this);
+	}
+    }
+
+
+    public void refresh()
+    {
+  	unmap();
+  	map();
+    }
+
+	// overridden in subclass CollectionTreeNode to reset and reencode display strings
+    public void resetDescendantEncodings() {}
+    public void refreshDescendantEncodings() {}
+	
+	
+    public void setModel(FileSystemModel model) {
+	this.model = model;
+    }
+
+    public void setParent(MutableTreeNode parent) {
+	this.parent = parent;
+    }
+
+
+    /** Return the total number of child files for the file this node represents, irrespective of filters set. */
+    public int size() {
+	if (child_nodes_unfiltered != null) {
+	    return child_nodes_unfiltered.size();
+	}
+	return 0;
+    }
+
+
+    public String toString()
+    {
+	if (isFileSystemRoot()) {
+	    return file.getAbsolutePath();
+	}
+	else {
+	    if(displayFileName == null) {    
+		displayFileName = calcDisplayString();
+	    }
+	    return displayFileName; //return file.getName();	    
+	}
+    }
+
+
+    /** Unmap this node's children. */
+    public void unmap()
+    {
+	DebugStream.println("Unmapping " + this + "...");
+	child_nodes_unfiltered = null;
+	child_nodes = null;
+    }
+
+
+    private class FileEnumeration
+	implements Enumeration
+    {
+	private int index = 0;
+
+	/** Tests if this enumeration contains more elements. */
+	public boolean hasMoreElements() {
+	    return (index < child_nodes.size());
+	}
+
+	/** Returns the next element of this enumeration if this enumeration object has at least one more element to provide. */
+	public Object nextElement() {
+	    Object result = null;
+	    if (index < child_nodes.size()) {
+		result = child_nodes.get(index);
+		index++;
+	    }
+	    return result;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileOpenActionListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileOpenActionListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileOpenActionListener.java	(revision 31635)
@@ -0,0 +1,101 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.file;
+
+import java.awt.event.*;
+import java.io.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Gatherer;
+
+/** This class listens for mouse clicks and responds to double-clicks (spawn a new application to view the selected file) and right mouse button clicks (popup menu). 
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3b
+ */
+public class FileOpenActionListener
+    extends MouseAdapter 
+    implements TreeExpansionListener {
+    /** This flag gets toggled to <i>true</i> if the listener determines that the next mouse clicked event it will recieve is actually caused by the tree expanding or collapsing. */
+    private boolean ignore = false;
+    /** The constructor. */
+    public FileOpenActionListener() {
+    }
+    /** Any subclass of MouseAdapter can override this method to respond to mouse click events. In this case we want to start an external application if someone double clicks on an appropriate file record. 
+     * @param event A <strong>MouseEvent</strong> containing further information about the mouse click performed.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.file.FileNode
+     * @see org.greenstone.gatherer.gui.tree.DragTree
+     */ 
+    public void mouseClicked(MouseEvent event) {
+	///ystem.err.println("Mouse clicked");
+	if(!ignore) {
+	    if(event.getClickCount() >= 2) {
+		// Find the file we're clicking on.
+		JTree tree = (JTree)event.getSource();
+		TreePath path = tree.getClosestPathForLocation(event.getX(), event.getY());
+		if(path != null) {
+		    FileNode record = (FileNode)path.getLastPathComponent();
+		    ///ystem.err.println("Double clicked on " + record);
+		    File file = record.getFile();
+		    if (file != null && file.isFile()) {
+			///ystem.err.println("Running " + file);
+			Gatherer.f_man.openFileInExternalApplication(file);
+		    }
+		}
+	    }
+	}
+	else {
+	    ///ystem.err.println("Caused by tree expansion / collapse. Ignoring.");
+	    ignore = false;
+	}
+    }
+    /** Any implementation of TreeExpansionListener must include this method so that we can be informed when a tree node has been collapsed, thus indicating that any mouse click events about to be recieved are most likely related to this event.
+     * @param event A <strong>TreeExpansionEvent</strong> containing information about the node collapsed.
+     */
+    public void treeCollapsed(TreeExpansionEvent event) {
+	///ystem.err.println("Tree Collapsed");
+	ignore = true;
+    }
+    /** Any implementation of TreeExpansionListener must include this method so that we can be informed when a tree node has been expanded, thus indicating that any mouse click events about to be recieved are most likely related to this event.
+     * @param event A <strong>TreeExpansionEvent</strong> containing information about the node expanded.
+     */
+    public void treeExpanded(TreeExpansionEvent event) {
+	///ystem.err.println("Tree Expanded");
+	ignore = true;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileQueue.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileQueue.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileQueue.java	(revision 31635)
@@ -0,0 +1,846 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.file;
+
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.gui.GProgressBar;
+import org.greenstone.gatherer.gui.tree.DragTree;
+import org.greenstone.gatherer.metadata.MetadataValue;
+import org.greenstone.gatherer.metadata.MetadataXMLFileManager;
+import org.greenstone.gatherer.util.DragComponent;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.SynchronizedTreeModelTools;
+import org.greenstone.gatherer.util.Utility;
+
+/** A threaded object which processes a queue of file actions such as copying and movement. It also handles updating the various trees involved so they are an accurate representation of the file system they are meant to match.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class FileQueue
+    extends Thread
+{
+    /** The size of the io buffer, in bytes. */
+    static final private int BUFFER_SIZE = 1024;
+
+    /** When someone requests the movement queue to be dumped this cancel flag is set to true. */
+    private boolean cancel_action = false;
+    /** The button which controls the stopping of the file queue. */
+    private JButton stop_button = null;
+    /** true if the user has selected yes to all from a file 'clash' dialog. */
+    private boolean yes_to_all = false;
+    /** A label explaining the current moving files status. */
+    private JLabel file_status = null;
+    /** A list containing a queue of waiting movement jobs. */
+    private ArrayList queue = null;
+    /** A progress bar which shows how many bytes, out of the total size of bytes, has been moved. */
+    private GProgressBar progress = null;
+
+
+    /** Constructor.
+     */
+    public FileQueue() {
+	DebugStream.println("FileQueue started.");
+	this.queue = new ArrayList();
+	file_status = new JLabel(Dictionary.get("FileActions.No_Activity"));
+	progress = new GProgressBar();
+	progress.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	progress.setForeground(Configuration.getColor("coloring.collection_tree_foreground", false));
+	progress.setString(Dictionary.get("FileActions.No_Activity"));
+	progress.setStringPainted(true);
+    }
+
+
+    /** Add a new job to the queue, specifiying as many arguments as is necessary to complete this type of job (ie delete needs no target information).
+     * @param id A long id unique to all jobs created by a single action.
+     * @param source The DragComponent source of this file, most likely a DragTree.
+     * @param child The FileNode you wish to mode.
+     * @param target The DragComponent to move the file to, again most likely a DragTree.
+     * @param parent The files new FileNode parent within the target.
+     * @param type The type of this movement as an int, either COPY or DELETE.
+     */
+    public void addJob(long id, DragComponent source, FileNode[] children, DragComponent target, FileNode parent, byte type)
+    {
+	// Queue the sub-job(s) (this may fail if we are asked to delete a read only file)
+	for (int i = 0; i < children.length; i++) {
+	    addJob(id, source, children[i], target, parent, type, -1);
+	}
+    }
+    
+    synchronized private void addJob(long id, DragComponent source, FileNode child, DragComponent target, FileNode parent, byte type, int position) {
+ 	FileJob job = new FileJob(id, source, child, target, parent, type);
+	DebugStream.println("Adding job: " + job);
+ 	if(position != -1 && position <= queue.size() + 1) {
+ 	    queue.add(position, job);
+ 	}
+ 	else {
+	    queue.add(job);
+ 	}
+ 	notify();
+    }
+
+    /** Calculates the total deep file size of the selected file nodes.
+     * @param files a FileNode[] of selected files
+     * @return true if a cancel was signalled, false otherwise
+     * @see org.greenstone.gatherer.file.FileManager.Task#run()
+     */
+    public boolean calculateSize(FileNode[] files)
+    {
+	file_status.setText(Dictionary.get("FileActions.Calculating_Size"));
+	progress.setString(Dictionary.get("FileActions.Calculating_Size"));
+
+	// Calculate the total file size of all the selected file nodes
+	Vector remaining = new Vector();
+	for (int i = 0; !cancel_action && i < files.length; i++) {
+	    remaining.add(files[i]);
+	}
+	while (!cancel_action && remaining.size() > 0) {
+	    FileNode node = (FileNode) remaining.remove(0);
+	    if (node.isLeaf()) {
+		progress.addMaximum(node.getFile().length());
+	    }
+	    else {
+		for (int i = 0; !cancel_action && i < node.getChildCount(); i++) {
+		    remaining.add(node.getChildAt(i));
+		}
+	    }
+	}
+
+	// Now we return if calculation was cancelled so that the FileManagers Task can skip the addJob phase correctly.
+	if (cancel_action) {
+	    cancel_action = false; // reset
+	    return true;
+	}
+	else {
+	    return false;
+	}
+    }
+
+    /** This method is called to cancel the job queue at the next available moment. */
+    public void cancelAction() {
+	cancel_action = true;
+	clearJobs();
+    }
+
+
+    /** Format the given filename path string so that it is no longer than the given width. If it is wider replace starting directories with ...
+     * @param key The key <strong>String</Strong> used to retrieve a phrase from the dictionary for this item.
+     * @param raw The raw filename path <strong>String</strong>.
+     * @param width The maximum width as an <i>int</i>.
+     * @return A path <strong>String</strong> no longer than width.
+     */
+    String formatPath(String key, String raw, int width) // package access
+    {
+	JLabel label = new JLabel(Dictionary.get(key, raw));
+	int position = -1;
+	while(label.getPreferredSize().width > width && (position = raw.indexOf(File.separator)) != -1) {
+	    raw = "..." + raw.substring(position + 1);
+	    label.setText(Dictionary.get(key, raw));
+	}
+	if(raw.indexOf(File.separator) == -1 && raw.startsWith("...")) {
+	    raw = raw.substring(3);
+	}
+	return raw;
+    }
+
+
+    /** Access to the file state label. */
+    public JLabel getFileStatus() {
+	return file_status;
+    }
+
+    /** Access to the progress bar. */
+    public GProgressBar getProgressBar() {
+	return progress;
+    }
+
+
+    synchronized private void addFileJob(long id, DragComponent source, FileNode child, DragComponent target, FileNode parent, byte type)
+    {
+	queue.add(new FileJob(id, source, child, target, parent, type));
+	notify();
+    }
+
+
+    private void doEmptyDirectoryDelete(FileJob file_job)
+    {
+	FileNode source_node = file_job.getOrigin();
+	File source_directory = source_node.getFile();
+
+	// If the directory isn't empty then this will fail
+	if (source_directory.delete() == false) {
+	    // The source directory couldn't be deleted, so give the user the option of continuing or cancelling
+	    if (showErrorDialog(Dictionary.get("FileActions.Could_Not_Delete", source_directory.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+
+	// Remove the node from the model
+	SynchronizedTreeModelTools.removeNodeFromParent(file_job.source.getTreeModel(), source_node);
+    }
+
+
+    private void doDirectoryDelete(FileJob file_job)
+    {
+	FileNode source_node = file_job.getOrigin();
+	File source_directory = source_node.getFile();
+
+	// The last thing we will do is delete this directory (which should be empty by then)
+	addFileJob(file_job.ID(), file_job.source, source_node, null, null, FileJob.DELETE_EMPTY_DIRECTORY);
+
+	// Add a new Delete job for each child of this directory (except metadata.xml files)
+	source_node.refresh();
+	for (int i = 0; i < source_node.size(); i++) {
+	    FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i);
+	    if (!child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) {
+		addFileJob(file_job.ID(), file_job.source, child_file_node, null, null, FileJob.DELETE);
+	    }
+	}
+
+	// Treat metadata.xml files specially: delete them first
+	for (int i = 0; i < source_node.size(); i++) {
+	    FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i);
+	    if (child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) {
+		addFileJob(file_job.ID(), file_job.source, child_file_node, null, null, FileJob.DELETE);
+		break;
+	    }
+	}
+    }
+
+
+    private void doDirectoryCopy(FileJob file_job)
+    {
+	FileNode source_node = file_job.getOrigin();
+	FileNode target_node = file_job.getDestination();
+
+	File source_directory = source_node.getFile();
+	File target_directory = new File(target_node.getFile(), source_directory.getName());
+
+	// Check that the source directory doesn't contain the target directory (will create a cyclic loop)
+	if (target_directory.getAbsolutePath().startsWith(source_directory.getAbsolutePath())) {
+	    if (showErrorDialog(Dictionary.get("FileActions.Cyclic_Path", source_directory.getName())) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+
+	// The target directory shouldn't already exist
+	if (target_directory.exists()) {
+	    if (showErrorDialog(Dictionary.get("FileActions.Folder_Already_Exists", target_directory.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+	target_directory.mkdirs();
+
+	// Create a node for the new directory in the collection tree
+	FileSystemModel target_model = (FileSystemModel) file_job.target.getTreeModel();
+	CollectionTreeNode new_target_node = new CollectionTreeNode(target_directory);
+	SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node);
+	new_target_node.setParent(target_node);
+
+	// Copy the non-folder level metadata assigned to the original directory to the new directory
+	ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToExternalFile(source_directory);
+	MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata);
+
+	// Add a new Copy job for each child of this directory (except metadata.xml files)
+	source_node.refresh();
+	for (int i = 0; i < source_node.size(); i++) {
+	    FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i);
+	    if (!child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) {
+		addFileJob(file_job.ID(), file_job.source, child_file_node, file_job.target, new_target_node, FileJob.COPY);
+	    }
+	}
+    }
+
+
+    private void doDirectoryMove(FileJob file_job)
+    {
+	FileNode source_node = file_job.getOrigin();
+	FileNode target_node = file_job.getDestination();
+
+	File source_directory = source_node.getFile();
+	File target_directory = new File(target_node.getFile(), source_directory.getName());
+	if (file_job.type == FileJob.RENAME) {
+	    // This is the only difference between moves and renames
+	    target_directory = target_node.getFile();
+	    target_node = (FileNode) source_node.getParent();
+	}
+
+	// Check the target directory isn't the source directory
+	if (target_directory.equals(source_directory)) {
+	    DebugStream.println("Target directory is the source directory!");
+	    return;
+	}
+
+	// The target directory shouldn't already exist
+	if (target_directory.exists()) {
+	    if (showErrorDialog(Dictionary.get("FileActions.Folder_Already_Exists", target_directory.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+	target_directory.mkdirs();
+
+	// Create a node for the new directory in the collection tree
+	FileSystemModel target_model = (FileSystemModel) file_job.target.getTreeModel();
+	CollectionTreeNode new_target_node = new CollectionTreeNode(target_directory);
+	SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node);
+	new_target_node.setParent(target_node);
+
+	// Move the folder level metadata assigned to the original directory to the new directory
+	ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_directory);
+	MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata);
+	MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata);
+
+	// The last thing we will do is delete this directory
+	addFileJob(file_job.ID(), file_job.source, source_node, null, null, FileJob.DELETE);
+
+	// Treat metadata.xml files specially: delete them last
+	source_node.refresh();
+	for (int i = 0; i < source_node.size(); i++) {
+	    FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i);
+	    if (child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) {
+		addFileJob(file_job.ID(), file_job.source, child_file_node, null, null, FileJob.DELETE);
+		break;
+	    }
+	}
+
+	// Add a new Move job for each child of this directory (except metadata.xml files)
+	for (int i = 0; i < source_node.size(); i++) {
+	    FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i);
+	    if (!child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) {
+		addFileJob(file_job.ID(), file_job.source, child_file_node, file_job.target, new_target_node, FileJob.MOVE);
+	    }
+	}
+    }
+
+
+    private void doFileDelete(FileJob file_job)
+    {
+	FileNode source_node = file_job.getOrigin();
+	File source_file = source_node.getFile();
+
+	// Almost all files will be deleted from the collection tree (exception: files in "Downloaded Files")
+	if (source_node instanceof CollectionTreeNode) {
+	    // If we're deleting a metadata.xml file we must unload it
+	    boolean metadata_xml_file = source_file.getName().equals(StaticStrings.METADATA_XML);
+	    if (metadata_xml_file) {
+		MetadataXMLFileManager.unloadMetadataXMLFile(source_file);
+	    }
+	    // Otherwise remove any metadata assigned directly to the file
+	    else {
+		ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_file);
+		MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata);
+	    }
+	}
+
+	// Delete the source file
+	if (!Utility.delete(source_file)) {
+	    // The source file couldn't be deleted, so give the user the option of continuing or cancelling
+	    if (showErrorDialog(Dictionary.get("FileActions.File_Not_Deleted_Message", source_file.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+
+	// Remove the node from the model
+	SynchronizedTreeModelTools.removeNodeFromParent(file_job.source.getTreeModel(), source_node);
+    }
+
+
+    private void doFileCopy(FileJob file_job)
+    {
+	FileNode source_node = file_job.getOrigin();
+	FileNode target_node = file_job.getDestination();
+
+	File source_file = source_node.getFile();
+	File target_file = new File(target_node.getFile(), source_file.getName());
+
+	// The target file shouldn't already exist -- if it does ask the user whether they want to overwrite
+	boolean overwrite_file = false;
+	if (target_file.exists()) {
+	    int result = showOverwriteDialog(target_file.getName());
+	    if (result == JOptionPane.NO_OPTION) {
+		// Don't overwrite
+		return;
+	    }
+	    if (result == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+		return;
+	    }
+
+	    overwrite_file = true;
+	}
+
+	// Copy the file
+	try {
+	    copyFile(source_file, target_file, true);
+	}
+	catch (FileAlreadyExistsException exception) {
+	    // This should not ever happen, since we've called copyFile with overwrite set
+	    DebugStream.printStackTrace(exception);
+	    return;
+	}
+	catch (FileNotFoundException exception) {
+	    DebugStream.printStackTrace(exception);
+	    if (showErrorDialog(Dictionary.get("FileActions.File_Not_Found_Message", source_file.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    // Refresh the source tree model
+	    FileSystemModel source_model = file_job.source.getTreeModel();
+	    source_model.refresh(new TreePath(((FileNode) file_job.getOrigin().getParent()).getPath()));
+	    return;
+	}
+	catch (InsufficientSpaceException exception) {
+	    DebugStream.printStackTrace(exception);
+	    if (showErrorDialog(Dictionary.get("FileActions.Insufficient_Space_Message", exception.getMessage())) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+	catch (IOException exception) {
+	    DebugStream.printStackTrace(exception);
+	    if (showErrorDialog(exception.getMessage()) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+	catch (ReadNotPermittedException exception) {
+	    DebugStream.printStackTrace(exception);
+	    if (showErrorDialog(Dictionary.get("FileActions.Read_Not_Permitted_Message", source_file.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+	catch (UnknownFileErrorException exception) {
+	    DebugStream.printStackTrace(exception);
+	    if (showErrorDialog(Dictionary.get("FileActions.Unknown_File_Error_Message")) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+	catch (WriteNotPermittedException exception) {
+	    DebugStream.printStackTrace(exception);
+	    if (showErrorDialog(Dictionary.get("FileActions.Write_Not_Permitted_Message", target_file.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+
+	CollectionTreeNode new_target_node = new CollectionTreeNode(target_file);
+	if (overwrite_file == false) {
+	    // Add the new node into the tree
+	    FileSystemModel target_model = file_job.target.getTreeModel();
+	    SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node);
+	}
+	Gatherer.c_man.fireFileAddedToCollection(target_file);
+
+	// Copy the non-folder level metadata assigned to the original file to the new file
+	if (file_job.type == FileJob.COPY) {
+	    // do metadata too
+	    ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToExternalFile(source_file);
+	    MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata);
+	}
+    }
+
+
+    private void doFileMove(FileJob file_job)
+    {
+	FileNode source_node = file_job.getOrigin();
+	FileNode target_node = file_job.getDestination();
+
+	File source_file = source_node.getFile();
+	File target_file = new File(target_node.getFile(), source_file.getName());
+	if (file_job.type == FileJob.RENAME) {
+	    // This is the only difference between moves and renames
+	    target_file = target_node.getFile();
+	    target_node = (FileNode) source_node.getParent();
+	}
+
+	// Check the target file isn't the source file
+	if (target_file.equals(source_file)) {
+	    DebugStream.println("Target file is the source file!");
+	    return;
+	}
+
+	// The target file shouldn't already exist
+	if (target_file.exists()) {
+	    int result = showOverwriteDialog(target_file.getName());
+	    if (result == JOptionPane.NO_OPTION) {
+		// Don't overwrite
+		return;
+	    }
+	    if (result == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+		return;
+	    }
+	}
+
+	// Move the file by renaming it
+	if (!source_file.renameTo(target_file)) {
+	    String args[] = { source_file.getName(), target_file.getAbsolutePath() };
+	    if (showErrorDialog(Dictionary.get("FileActions.File_Move_Error_Message", args)) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    return;
+	}
+
+	// Remove the node from the source model and add it to the target model
+	SynchronizedTreeModelTools.removeNodeFromParent(file_job.source.getTreeModel(), source_node);
+	CollectionTreeNode new_target_node = new CollectionTreeNode(target_file);
+	FileSystemModel target_model = file_job.target.getTreeModel();
+	SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node);
+
+	// Move the non-folder level metadata assigned to the original file to the new file
+	ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_file);
+	MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata);
+	MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata);
+    }
+
+    
+    /** all this does is move the metadata, and delete the source */
+    private void doFileReplace(FileJob file_job)
+    {
+	FileNode source_node = file_job.getOrigin();
+	FileNode target_node = file_job.getDestination();
+
+	File source_file = source_node.getFile();
+	File target_file = target_node.getFile();
+
+	// Move the non-folder level metadata assigned to the original file to the new file
+	CollectionTreeNode new_target_node = new CollectionTreeNode(target_file);
+	ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_file);
+	MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata);
+	MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata);
+
+	// now delete the original
+	doFileDelete(file_job);
+    }
+
+
+    private void processFileJob(FileJob file_job)
+    {
+	DebugStream.println("Processing file job " + file_job + "...");
+
+	// Ensure that the source file exists
+	File source_file = file_job.getOrigin().getFile();
+	if (!source_file.exists()) {
+	    // The source file doesn't exist, so give the user the option of continuing or cancelling
+	    if (showErrorDialog(Dictionary.get("FileActions.File_Not_Found_Message", source_file.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
+		clearJobs();  // Aborting action
+	    }
+	    // Refresh the source tree model
+	    FileSystemModel source_model = file_job.source.getTreeModel();
+	    source_model.refresh(new TreePath(((FileNode) file_job.getOrigin().getParent()).getPath()));
+	    return;
+	}
+
+	// Enable the "Stop" button
+	stop_button.setEnabled(true);
+
+	// Delete empty directory job
+	if (file_job.type == FileJob.DELETE_EMPTY_DIRECTORY) {
+	    file_status.setText(Dictionary.get("FileActions.Deleting", formatPath("FileActions.Deleting", source_file.getAbsolutePath(), file_status.getSize().width)));
+	    doEmptyDirectoryDelete(file_job);
+	    return;
+	}
+
+	// Delete job
+	if (file_job.type == FileJob.DELETE) {
+	    file_status.setText(Dictionary.get("FileActions.Deleting", formatPath("FileActions.Deleting", source_file.getAbsolutePath(), file_status.getSize().width)));
+	    if (source_file.isFile()) {
+		long source_file_size = source_file.length();
+		doFileDelete(file_job);
+		progress.addValue(source_file_size);  // Update progress bar
+	    }
+	    else {
+		doDirectoryDelete(file_job);
+	    }
+	    return;
+	}
+
+	// Copy job
+	if (file_job.type == FileJob.COPY || file_job.type == FileJob.COPY_FILE_ONLY) {
+	    file_status.setText(Dictionary.get("FileActions.Copying", formatPath("FileActions.Copying", source_file.getAbsolutePath(), file_status.getSize().width)));
+	    if (source_file.isFile()) {
+		long source_file_size = source_file.length();
+		doFileCopy(file_job);
+		progress.addValue(source_file_size);  // Update progress bar
+	    }
+	    else {
+		doDirectoryCopy(file_job);
+	    }
+	    return;
+	}
+
+	// Move (or rename) job
+	if (file_job.type == FileJob.MOVE || file_job.type == FileJob.RENAME) {
+	    file_status.setText(Dictionary.get("FileActions.Moving", formatPath("FileActions.Moving", source_file.getAbsolutePath(), file_status.getSize().width)));
+	    if (source_file.isFile()) {
+		long source_file_size = source_file.length();
+		doFileMove(file_job);
+		progress.addValue(source_file_size);  // Update progress bar
+	    }
+	    else {
+		doDirectoryMove(file_job);
+	    }
+	    return;
+	}
+
+	// Replace job
+	if (file_job.type == FileJob.REPLACE) {
+	    file_status.setText(Dictionary.get("FileActions.Replacing", formatPath("FileActions.Replacing", source_file.getAbsolutePath(), file_status.getSize().width)));
+	    doFileReplace(file_job);
+	    return;
+	}
+    }
+
+
+    private int showErrorDialog(String error_message)
+    {
+	Object[] options = { Dictionary.get("General.OK"), Dictionary.get("General.Cancel") };
+	int result = JOptionPane.showOptionDialog(Gatherer.g_man, error_message, Dictionary.get("General.Error"), JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, null, options, options[0]);
+	if (result == 0) {
+	    return JOptionPane.OK_OPTION;
+	}
+	else {
+	    return JOptionPane.CANCEL_OPTION;
+	}
+    }
+
+
+    private int showOverwriteDialog(String target_file_name)
+    {
+	// Has "yes to all" been set?
+	if (yes_to_all) {
+	    return JOptionPane.YES_OPTION;
+	}
+
+	Object[] options = { Dictionary.get("General.Yes"), Dictionary.get("FileActions.Yes_To_All"), Dictionary.get("General.No"), Dictionary.get("General.Cancel") };
+	int result = JOptionPane.showOptionDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Exists", target_file_name), Dictionary.get("General.Warning"), JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
+	if (result == 0) {
+	    return JOptionPane.YES_OPTION;
+	}
+	else if (result == 1) {
+	    yes_to_all = true;
+	    return JOptionPane.YES_OPTION;
+	}
+	else if (result == 2) {
+	    return JOptionPane.NO_OPTION;
+	}
+	else {
+	    return JOptionPane.CANCEL_OPTION;
+	}
+    }
+
+
+    public void run()
+    {
+	super.setName("FileQueue");
+
+	while (!Gatherer.exit) {
+	    // Retrieve the next job
+	    int position = queue.size() - 1;
+	    if (position >= 0) {
+		// We have a file job, so process it
+		processFileJob((FileJob) queue.remove(position));
+	    }
+	    else {
+		// No jobs, so reset and wait until we are notified of one
+		synchronized(this) {
+		    // Force both workspace and collection trees to refresh
+		    if (Gatherer.g_man != null && Gatherer.c_man.ready()) {
+			Gatherer.g_man.refreshWorkspaceTree(DragTree.COLLECTION_CONTENTS_CHANGED);
+			// Gatherer.g_man.refreshCollectionTree(DragTree.COLLECTION_CONTENTS_CHANGED);
+		    }
+
+		    // Reset status area
+		    file_status.setText(Dictionary.get("FileActions.No_Activity"));
+		    progress.reset();
+		    progress.setString(Dictionary.get("FileActions.No_Activity"));
+
+		    // Reset "yes to all" and "cancel" flags
+		    yes_to_all = false;
+		    cancel_action = false;
+
+		    // Wait for a new file job
+		    try {
+			wait();
+		    }
+		    catch (InterruptedException exception) {}
+		}
+	    }
+	}
+    }
+
+
+    /** Register the button that will be responsible for stopping executing file actions. 
+     * @param stop_button a JButton
+     */
+    public void registerStopButton(JButton stop_button) {
+	this.stop_button = stop_button;
+    }
+
+
+    synchronized private void clearJobs() {
+	queue.clear();
+    }
+    
+    /** Copy the contents from the source directory to the destination 
+     * directory.
+     * @param source The source directory
+     * @param destination The destination directory
+     * @see org.greenstone.gatherer.Gatherer
+     */
+    public void copyDirectoryContents(File source, File destination, boolean overwrite) 
+	throws FileAlreadyExistsException, FileNotFoundException, InsufficientSpaceException, IOException, ReadNotPermittedException, UnknownFileErrorException, WriteNotPermittedException
+    {
+	if (!source.isDirectory()) return;
+	// check that dest dirs exist
+	destination.mkdirs();
+	
+	File [] src_files = source.listFiles();
+	if (src_files.length == 0) return; // nothing to copy
+	for (int i=0; i<src_files.length; i++) {
+	    File f = src_files[i];
+	    String f_name = f.getName();
+	    File new_file = new File(destination, f_name);
+	    if (f.isDirectory()) {
+		copyDirectoryContents(f, new_file, overwrite);
+	    } else if (f.isFile()) {
+		copyFile(f, new_file, overwrite);
+	    }
+	}
+    }
+
+    /** Preserving old default behaviour of copyDirContents method, where overwrite is false. */
+    public void copyDirectoryContents(File source, File destination) throws FileAlreadyExistsException, FileNotFoundException, InsufficientSpaceException, IOException, ReadNotPermittedException, UnknownFileErrorException, WriteNotPermittedException 
+    {
+	copyDirectoryContents(source, destination, false);
+    }
+
+    
+    /** Copy a file from the source location to the destination location.
+     * @param source The source File.
+     * @param destination The destination File.
+     * @see org.greenstone.gatherer.Gatherer
+     */
+    public void copyFile(File source, File destination, boolean overwrite)
+	throws FileAlreadyExistsException, FileNotFoundException, InsufficientSpaceException, IOException, ReadNotPermittedException, UnknownFileErrorException, WriteNotPermittedException
+    {
+	if (source.isDirectory()) {
+	    destination.mkdirs();
+	    return;
+	}
+
+	// Check if the origin file exists.
+	if (!source.exists()) {
+	    DebugStream.println("Couldn't find the source file.");
+	    throw new FileNotFoundException();
+	}
+
+	// Make sure the destination file does not exist.
+	if (destination.exists() && !overwrite) {
+	    throw new FileAlreadyExistsException();
+	}
+
+	// Open an input stream to the source file
+	FileInputStream f_in = null;
+	try {
+	    f_in = new FileInputStream(source);
+	}
+	catch (FileNotFoundException exception) {
+	    // A FileNotFoundException translates into a ReadNotPermittedException in this case
+	    throw new ReadNotPermittedException(exception.toString());		
+	}
+
+	// Create an necessary directories for the target file
+	File dirs = destination.getParentFile();
+	dirs.mkdirs();
+
+	// Open an output stream to the target file
+	FileOutputStream f_out = null;
+	try {
+	    f_out = new FileOutputStream(destination);
+	}
+	catch (FileNotFoundException exception) {
+	    // A FileNotFoundException translates into a WriteNotPermittedException in this case
+	    f_in.close();
+	    throw new WriteNotPermittedException(exception.toString());
+	}
+
+	// Copy the file
+	byte data[] = new byte[BUFFER_SIZE];
+	int data_size = 0;
+	while ((data_size = f_in.read(data, 0, BUFFER_SIZE)) != -1 && !cancel_action) {
+	    long destination_size = destination.length();
+	    try {
+		f_out.write(data, 0, data_size);
+	    }
+	    // If an IO exception occurs, we can do some maths to determine if the number of bytes written to the file was less than expected. If so we assume a InsufficientSpace exception. If not we just throw the exception again.
+	    catch (IOException io_exception) {
+		f_in.close();
+		f_out.close();
+
+		if (destination_size + (long) data_size > destination.length()) {
+		    // Determine the difference (which I guess is in bytes).
+		    long difference = (destination_size + (long) data_size) - destination.length();
+		    // Transform that into a human readable string.
+		    String message = Utility.formatFileLength(difference);
+		    throw new InsufficientSpaceException(message);
+		}
+		else {
+		    throw(io_exception);
+		}
+	    }
+	}
+
+	// Flush and close the streams to ensure all bytes are written.
+	f_in.close();
+	f_out.close();
+
+	// We have now, in theory, produced an exact copy of the source file. Check this by comparing sizes.
+	if(!destination.exists() || (!cancel_action && source.length() != destination.length())) {
+	    throw new UnknownFileErrorException();
+	}
+
+	// If we were cancelled, ensure that none of the destination file exists.
+	if (cancel_action) {
+	    destination.delete();
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileSystem.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileSystem.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileSystem.java	(revision 31635)
@@ -0,0 +1,63 @@
+package org.greenstone.gatherer.file;
+
+import java.io.File;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+
+
+public class FileSystem
+{
+    static public WorkspaceTreeNode getLocalFilespaceNode(FileSystemModel model)
+    {
+	// Get all the available roots mounted on the filesystem
+	File[] roots = File.listRoots();
+	if (roots == null) {
+	    return null;
+	}
+
+	// If there is just one root use it as the tree root (Unix)
+	if (roots.length == 1) {
+	    return new WorkspaceTreeNode(roots[0], Dictionary.get("Tree.Root"));
+	}
+
+	// Otherwise build a dummy node which has the roots as children (Windows)
+	else {
+	    return new WorkspaceTreeNode(null, Dictionary.get("Tree.Root"));
+	}
+    }
+
+
+    static public WorkspaceTreeNode getHomeFolderNode()
+    {
+	// Get the name of the OS we're running on
+	String os_name = System.getProperty("os.name");
+	if (os_name == null) {
+	    return null;
+	}
+
+	// Make sure we're running on a multiuser OS where the idea of a "user home" is valid
+	DebugStream.println("Property os.name: " + os_name);
+	String os_name_lc = os_name.toLowerCase();
+	if (os_name_lc.equals("windows 95") || os_name_lc.equals("windows 98") || os_name_lc.equals("windows me")) {
+	    return null;
+	}
+
+	// Get the user's home folder
+ 	String home_folder_str = System.getProperty("user.home");
+	if (home_folder_str == null || home_folder_str.length() == 0) {
+	    return null;
+	}
+
+	// Generate a special mapping to the user home folder
+	File home_folder = new File(home_folder_str);
+	return new WorkspaceTreeNode(home_folder, Dictionary.get("Tree.Home", home_folder.getName()));
+    }
+
+
+    static public WorkspaceTreeNode getDownloadedFilesNode()
+    {
+	// Return a mapping to the downloaded files folder
+	return new WorkspaceTreeNode(new File(Gatherer.getGLIUserCacheDirectoryPath()), Dictionary.get("Tree.DownloadedFiles"));
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileSystemModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileSystemModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/FileSystemModel.java	(revision 31635)
@@ -0,0 +1,278 @@
+package org.greenstone.gatherer.file;
+
+import java.io.File;
+import java.util.*;
+import java.util.regex.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.tree.DragTree;
+
+
+public class FileSystemModel
+    extends DefaultTreeModel 
+    implements TreeExpansionListener, TreeWillExpandListener
+{
+    private DragTree tree = null;
+    private FileFilter current_filter = null;
+    private FileFilter[] filters = null;
+    /** The filters in place for any file system model. */
+    private FileFilter[] default_filters = { new FileFilter("\\..*", true), new FileFilter("metadata\\.xml", true) };
+
+    public FileSystemModel(FileNode root) {
+	super(root);
+	root.setModel(this);
+	root.refresh();
+    }
+
+    public int getChildCount(Object parent) {
+	return ((TreeNode)parent).getChildCount();
+    }
+
+    public FileFilter[] getFilters() {
+	if(filters == null) {
+	    if(current_filter != null) {
+		filters = new FileFilter[default_filters.length + 1];
+		filters[default_filters.length] = current_filter;
+	    }
+	    else {
+		filters = new FileFilter[default_filters.length];
+	    }
+	    System.arraycopy(default_filters, 0, filters, 0, default_filters.length);
+	}
+	return filters;
+    }
+	 
+    /** Retrieve the node denoted by the given tree path. Note that this isn't equivelent to saying path.lastPathComponent, as the references within the path may be stale. */
+    public FileNode getNode(TreePath path) {
+	FileNode last = (FileNode)path.getLastPathComponent();
+	DebugStream.println("Last Path Component = " + last);
+	return last;
+	/*
+	///atherer.println("**** getNode(" + path + ") ****");
+	FileNode current = (FileNode)root;
+	// Special case for the root node. Check the first path component is the root node.
+	FileNode first_node = (FileNode)path.getPathComponent(0);
+	if(current.equals(first_node)) {
+	    DebugStream.println("First path component matches root node.");
+	    // For each path with this tree path
+	    for(int i = 1; current != null && i < path.getPathCount(); i++) {
+		// Retrieve the stale path
+		Object stale_object = path.getPathComponent(i);
+		FileNode stale_node = null;
+		if(stale_object instanceof FileNode) {
+		    stale_node = (FileNode) stale_object;
+		}
+		DebugStream.print("Searching for '" + stale_object + "': ");
+		// Locate the fresh node by searching current's children. Remember to ensure that current is mapped.
+		boolean found = false;
+
+		// First we search through the mapped children
+		for(int j = 0; !found && j < current.getChildCount(); j++) {
+		    FileNode child_node = (FileNode) current.getChildAt(j);
+		    DebugStream.print(child_node + " ");
+		    if((stale_node != null && stale_node.equals(child_node)) || stale_object.toString().equals(child_node.toString())) {
+			found = true;
+			current = child_node;
+			DebugStream.println("Found!");
+		    }
+		    child_node = null;
+		}
+		// Failing that we search through all the children, including filtered files
+		for(int j = 0; !found && j < current.size(); j++) {
+		    FileNode child_node = (FileNode) current.getChildAtUnfiltered(j);
+		    DebugStream.print(child_node + " ");
+		    if((stale_node != null && stale_node.equals(child_node)) || stale_object.toString().equals(child_node.toString())) {
+			found = true;
+			current = child_node;
+			DebugStream.println("Found!");
+		    }
+		    child_node = null;
+		}
+		// If no match is found, then set current to null and exit.
+		if(!found) {
+		    current = null;
+		    DebugStream.println("Not Found!");
+		}
+		else {
+		    DebugStream.println("Returning node: " + new TreePath(current.getPath()));
+		}
+		// Repeat as necessary
+	    }
+	}
+	return current;
+	*/
+    }
+
+    public void insertNodeInto(MutableTreeNode newChild, MutableTreeNode parent, int index) {
+	///ystem.err.println("insertNodeInto(" + newChild + ", " + parent + ", " + index + ")");
+	super.insertNodeInto(newChild, parent, index);
+    }
+
+
+    public void refresh(TreePath path)
+    {
+	// Can only refresh if the model is currently being displayed in a tree
+	if (tree == null) {
+	    return;
+	}
+
+	// If no path is set, take the path to the root node (ie. update the whole tree)
+	if (path == null) {
+	    // System.err.println("\nFileSystemModel.refresh(entire tree).");
+	    path = new TreePath(((FileNode) root).getPath());
+
+	    // Make sure the root node is expanded
+	    tree.expandPath(path);
+	}
+	// else {
+	//     System.err.println("\nFileSystemModel.refresh(" + path + ").");
+	// }
+
+	// Record all the expanded paths under this node
+	Enumeration old_expanded_paths_enumeration = tree.getExpandedDescendants(path);
+	if (old_expanded_paths_enumeration == null) {
+	    return;
+	}
+
+	// Map and unmap the node to refresh its contents
+	FileNode node = (FileNode) path.getLastPathComponent();
+	node.refresh();
+
+	// Fire the appropriate event
+	nodeStructureChanged(node);
+
+	// Sort the old expanded paths by length, smallest first
+	ArrayList old_expanded_paths_list = Collections.list(old_expanded_paths_enumeration);
+	Collections.sort(old_expanded_paths_list, new TreePathComparator());
+
+	// Restore each of the expanded paths
+	for (int i = 0; i < old_expanded_paths_list.size(); i++) {
+  	    TreePath old_expanded_path = (TreePath) old_expanded_paths_list.get(i);
+	    // System.err.println("Expanded path: " + old_expanded_path);
+
+	    // Build up the new path in the tree
+	    TreePath current_path = new TreePath(path.getPath());
+  	    FileNode current_node = node;
+
+	    // Traverse the tree to find the node to expand (or find it no longer exists)
+	    while (!current_path.toString().equals(old_expanded_path.toString())) {
+		// System.err.println("Current path: " + current_path);
+
+		FileNode old_expanded_node =
+		    (FileNode) old_expanded_path.getPathComponent(current_path.getPathCount());
+		// System.err.println("Looking for: " + old_expanded_node);
+
+		// Find the child node that matches the next element in the path
+		boolean found = false;
+		for (int j = 0; j < current_node.getChildCount(); j++) {
+		    FileNode child_node = (FileNode) current_node.getChildAt(j);
+		    // System.err.println("Child node: " + child_node);
+		    if (child_node.equals(old_expanded_node)) {
+			// System.err.println("Found!");
+			current_path = current_path.pathByAddingChild(child_node);
+			current_node = child_node;
+			found = true;
+			break;
+		    }
+		}
+
+		// The node was not found, so we cannot expand this path
+		if (!found) {
+		    // System.err.println("Not found...");
+		    break;
+		}
+	    }
+
+	    // If we have built up the correct path, expand it
+	    if (current_path.toString().equals(old_expanded_path.toString())) {
+		tree.expandPath(current_path);
+	    }
+	}
+    }
+
+
+    // The file tree can be filtered, so check that the node to be removed is actually part of the model
+    public void removeNodeFromParent(MutableTreeNode node)
+    {
+	TreeNode parent_node = ((FileNode) node).getParent();
+	if (parent_node != null && parent_node.getIndex((FileNode) node) != -1) {
+	    super.removeNodeFromParent(node);
+	}
+    }
+
+
+    private class TreePathComparator
+	implements Comparator {
+
+	public int compare(Object o1, Object o2)
+	{
+	    return (((TreePath) o1).getPathCount() - ((TreePath) o2).getPathCount());
+	}
+    }
+
+
+    public void setFilter(String pattern) {
+  	if(pattern != null) {
+  	    current_filter = new FileFilter(pattern, false);
+  	}
+  	else {
+  	    current_filter = null;
+  	}
+  	filters = null;
+    }
+
+    public void setTree(DragTree tree) {
+	this.tree = tree;
+    }
+
+    public String toString() {
+	if(tree != null) {
+	    return tree.toString();
+	}
+	return "FileSystemModel";
+    }
+
+    /** Called whenever an item in the tree has been collapsed. */
+    public void treeCollapsed(TreeExpansionEvent event) {
+	// Deallocate the affected nodes children. Don't need to do this in a swing worker, as the nodes children are currently not visable.
+	TreePath path = event.getPath();
+	FileNode node = (FileNode) path.getLastPathComponent();
+	node.unmap();
+	// Fire the appropriate event.
+	nodeStructureChanged(node);
+    }
+
+    /** Called whenever an item in the tree has been expanded. */
+    public void treeExpanded(TreeExpansionEvent event) {
+    }
+
+    /** Invoked whenever a node in the tree is about to be collapsed. */
+    public void treeWillCollapse(TreeExpansionEvent event) 
+	throws ExpandVetoException {
+	// Veto the event if the user is attempting to collapse the root node (regardless of whether it is visible).
+	TreePath path = event.getPath();
+	if(path.getPathCount() == 1) {
+	    throw new ExpandVetoException(event, "Cannot collapse root node!");
+	}
+    }
+
+
+    /** Invoked whenever a node in the tree is about to be expanded. */
+    public void treeWillExpand(TreeExpansionEvent event) 
+	throws ExpandVetoException
+    {
+	// Set the wait cursor
+	Gatherer.g_man.wait(true);
+
+	// Allocate the children (don't need a swing worker since the node's children are currently not visible)
+	TreePath path = event.getPath();
+	FileNode node = (FileNode) path.getLastPathComponent();
+	node.refresh();
+	nodeStructureChanged(node);
+
+	// Restore the cursor
+	Gatherer.g_man.wait(false);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/InsufficientSpaceException.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/InsufficientSpaceException.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/InsufficientSpaceException.java	(revision 31635)
@@ -0,0 +1,11 @@
+package org.greenstone.gatherer.file;
+
+import java.lang.Exception;
+
+public class InsufficientSpaceException
+    extends Exception {
+
+    public InsufficientSpaceException(String message) {
+	super(message);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/ReadNotPermittedException.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/ReadNotPermittedException.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/ReadNotPermittedException.java	(revision 31635)
@@ -0,0 +1,11 @@
+package org.greenstone.gatherer.file;
+
+import java.lang.Exception;
+
+public class ReadNotPermittedException
+    extends Exception
+{
+    public ReadNotPermittedException(String message) {
+	super(message);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/RecycleBin.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/RecycleBin.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/RecycleBin.java	(revision 31635)
@@ -0,0 +1,188 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.file;
+
+import java.awt.*;
+import java.awt.dnd.*;
+import java.awt.event.*;
+import java.awt.geom.AffineTransform;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.util.DragComponent;
+import org.greenstone.gatherer.util.DragGroup;
+import org.greenstone.gatherer.util.JarTools;
+
+
+public class RecycleBin
+    extends GLIButton
+    implements DragComponent, DropTargetListener {
+
+    private boolean ignore = false;
+    /** The group encompasses all of the objects you plan to drag and drop within, and ensures that only one has focus (as clearly identified by the colour of the selection field or, in this particular case, the background) and that actions only occur between components in the same group. */
+    private DragGroup group;
+    /** In order to make this button a drop target we have to create a DropTarget instance with the button as its target. */
+    private DropTarget drop_target;
+    private FileSystemModel model;
+    /** What sort of action should a drag resemble. Not really used as we override with custom drag icon. */
+    private int drag_action = DnDConstants.ACTION_MOVE;
+    /** The last point the mouse was at. Used to repaint 'spoilt' area. */
+    private Point pt_last = null;
+    /** The area covered by the drag ghost, our custom drag icon. */
+    private Rectangle ra_ghost = new Rectangle();
+
+
+    public RecycleBin()
+    {
+	super(JarTools.getImage("bin.gif"));
+	this.drop_target = new DropTarget(this, drag_action, this, true);
+
+	setBackground(Configuration.getColor("coloring.button_background", true));
+	setForeground(Configuration.getColor("coloring.button_foreground", true));
+	setOpaque(true);
+
+	this.model = new FileSystemModel(new RecycleBinNode(new File(Gatherer.getGLIUserDirectoryPath() + "recycle")));
+    }
+
+
+    /** In order for the appearance to be consistant, given we may be in the situation where the pointer has left our focus but the ghost remains, this method allows other members of the GGroup to tell this component to repair the 'spoilt' region left by its ghost. */
+    public void clearGhost(){
+    }
+
+
+    /** Any implementation of DropTargetListener must include this method so we can be notified when the drag focus enters this component. We want to provide some sort of indication whether the current component is an acceptable drop target as well as indicating focus. */
+    public void dragEnter(DropTargetDragEvent event) {
+	group.grabFocus(this);
+	setBackground(Configuration.getColor("coloring.button_selected_background", true));
+	setForeground(Configuration.getColor("coloring.button_selected_foreground", true));
+    }
+
+    /** Any implementation of DropTargetListener must include this method so we can be notified when the drag focus leaves this component. We need to indicate that we have lost focus. */
+    public void dragExit(DropTargetEvent event) {
+	setBackground(Configuration.getColor("coloring.button_background", true));
+	setForeground(Configuration.getColor("coloring.button_foreground", true));
+    }
+
+    /** Any implementation of DropTargetListener must include this method so we can be notified when the drag moves in this component. This is where we repaint our ghost icon at the tip of the mouse pointer. */
+    public void dragOver(DropTargetDragEvent event) {
+	Graphics2D g2 = (Graphics2D) getGraphics();
+	Point pt = event.getLocation();
+	if(pt_last != null && pt.equals(pt_last)) {
+	    return;
+	}
+	pt_last = pt;
+	if(!DragSource.isDragImageSupported()) {
+	    // Erase the last ghost image and or cue line
+	    paintImmediately(ra_ghost.getBounds());
+	    // Remember where you are about to draw the new ghost image
+	    ra_ghost.setRect(pt.x - group.mouse_offset.x, pt.y - group.mouse_offset.y, group.image_ghost.getWidth(), group.image_ghost.getHeight());
+	    // Draw the ghost image
+	    g2.drawImage(group.image_ghost, AffineTransform.getTranslateInstance(ra_ghost.getX(), ra_ghost.getY()), null);
+	}
+    }
+
+    /** Any implementation of DropTargetListener must include this method so we can be notified when the drag ends, ie the transferable is dropped. This in turn triggers a series of add events preceded by a pre() and followed by a post(). */
+    public void drop(DropTargetDropEvent event) {
+	ignore = true;
+	group.grabFocus(this);
+	setBackground(Configuration.getColor("coloring.button_background", true));
+	setForeground(Configuration.getColor("coloring.button_foreground", true));
+
+	try {
+	    DragComponent source = group.getSource();
+	    TreePath[] selection = group.getSelection();
+	    FileNode[] source_nodes = new FileNode[selection.length];
+	    for(int i = 0; i < source_nodes.length; i++) {
+		source_nodes[i] = (FileNode) selection[i].getLastPathComponent();
+	    }
+	    event.acceptDrop(drag_action);
+	    // Action delete
+	    Gatherer.f_man.action(source, source_nodes, this, null);
+	    group.setSource(null);
+	    group.setSelection(null);
+	}
+	catch(Exception error) {
+	    error.printStackTrace();
+	    event.rejectDrop();
+	}
+	ignore = false;
+	// Clear up the group.image_ghost
+	paintImmediately(ra_ghost.getBounds());
+	event.getDropTargetContext().dropComplete(true);
+    }
+
+    /** Any implementation of DropTargetListener must include this method so we can be notified when the action to be taken upon drop changes. We never change so we don't do anything here. */
+    public void dropActionChanged(DropTargetDragEvent event) {
+    }
+
+    /** Used to notify this component that it has gained focus by some method other that mouse focus. */
+    public void gainFocus() {
+    }
+
+    /** Any implementation of DragComponent must include this method so that a outsider can get at the underlying tree model behind the component. */
+    public FileSystemModel getTreeModel(){
+  	return (FileSystemModel) model;
+    }
+
+    public boolean ignore() {
+	return ignore;
+    }
+
+    /** This method is used to inform this component when it loses focus by means other than a drag mouse event, and should indicate this somehow. */
+    public void loseFocus() {
+    }
+
+    public void setGroup(DragGroup group) {
+	this.group = group;
+    }
+
+
+    public class RecycleBinNode
+	extends FileNode
+    {
+	public RecycleBinNode(File file)
+	{
+	    super(file);
+	}
+
+	public FileNode addChildNode(File file) { return null; }
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/UnknownFileErrorException.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/UnknownFileErrorException.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/UnknownFileErrorException.java	(revision 31635)
@@ -0,0 +1,11 @@
+package org.greenstone.gatherer.file;
+
+import java.lang.Exception;
+
+public class UnknownFileErrorException
+    extends Exception {
+
+    public UnknownFileErrorException() {
+	super();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WorkspaceTree.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WorkspaceTree.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WorkspaceTree.java	(revision 31635)
@@ -0,0 +1,450 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.file;
+
+import java.awt.event.*;
+import java.io.File;
+import java.util.Enumeration;
+import javax.swing.*;
+import javax.swing.tree.TreePath;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.gui.CreateShortcutPrompt;
+import org.greenstone.gatherer.gui.tree.DragTree;
+import org.greenstone.gatherer.util.Utility;
+
+
+public class WorkspaceTree
+    extends DragTree
+    implements MouseListener
+{
+    static public final int LIBRARY_CONTENTS_CHANGED = 10;
+    static public final int DOWNLOADED_FILES_CHANGED = 11;
+    static public final int FOLDER_SHORTCUTS_CHANGED = 12;
+
+
+    public WorkspaceTree()
+    {
+	super(WorkspaceTreeModel.getWorkspaceTreeModel(), true);
+	addMouseListener(this);
+
+	setBackgroundNonSelectionColor(Configuration.getColor("coloring.workspace_tree_background", false));
+	setBackgroundSelectionColor(Configuration.getColor("coloring.workspace_selection_background", false));
+	setTextNonSelectionColor(Configuration.getColor("coloring.workspace_tree_foreground", false));
+	setTextSelectionColor(Configuration.getColor("coloring.workspace_selection_foreground", false));
+
+	filter.setBackground(Configuration.getColor("coloring.workspace_heading_background", false));
+	filter.setEditable(Configuration.getMode() >= Configuration.LIBRARIAN_MODE);
+    }
+
+
+    public void addDirectoryMapping(String name, File file)
+    {
+	// Update the information stored in the user's configuration
+	Configuration.addDirectoryMapping(name, file);
+
+	// Now update the tree
+	refresh(FOLDER_SHORTCUTS_CHANGED);
+    }
+
+
+    public boolean isDraggable()
+    {
+	return true;
+    }
+
+
+    public boolean isDroppable()
+    {
+	return false;
+    }
+
+
+    public void mouseClicked(MouseEvent event)
+    {
+	if (SwingUtilities.isRightMouseButton(event)) {
+	    new WorkspaceTreeRightClickMenu(this, event);
+	}
+    }
+
+    public void mouseEntered(MouseEvent event) { }
+
+    public void mouseExited(MouseEvent event) { }
+
+    public void mousePressed(MouseEvent event) { }
+
+    public void mouseReleased(MouseEvent event) { }
+
+
+    public void refresh(int refresh_reason)
+    {
+	DebugStream.println("WorkspaceTree::refresh()... ");
+
+	// The method for displaying the tree has changed - redraw the tree
+  	if (refresh_reason == DragTree.TREE_DISPLAY_CHANGED) {
+  	    DebugStream.println("...Reason: tree display changed.");
+  	    updateUI();
+  	}
+
+	// The loaded collection has changed - currently do nothing
+	if (refresh_reason == DragTree.LOADED_COLLECTION_CHANGED) {
+	    DebugStream.println("...Reason: loaded collection changed.");
+	}
+
+	// The collection's contents have changed - refresh collection's import folder
+	if (refresh_reason == DragTree.COLLECTION_CONTENTS_CHANGED) {
+	    DebugStream.println("...Reason: collection contents changed.");
+	    String collection_import_directory_path = CollectionManager.getLoadedCollectionImportDirectoryPath();
+	    refreshEveryNodeShowingFolder(collection_import_directory_path);
+	}
+
+	// The collections in the library have changed - refresh the collect directory
+	if (refresh_reason == WorkspaceTree.LIBRARY_CONTENTS_CHANGED) {
+	    DebugStream.println("...Reason: library contents changed.");
+	    String collect_directory_path = Gatherer.getCollectDirectoryPath();
+	    refreshEveryNodeShowingFolder(collect_directory_path);
+	    WorkspaceTreeModel.refreshGreenstoneCollectionsNode();
+	}
+
+	// The downloaded files have changed - refresh that node
+	if (refresh_reason == WorkspaceTree.DOWNLOADED_FILES_CHANGED) {
+	    DebugStream.println("...Reason: downloaded files changed.");
+	    WorkspaceTreeModel.refreshDownloadedFilesNode();
+	}
+
+	// The folder shortcuts have changed - refresh only them
+	if (refresh_reason == WorkspaceTree.FOLDER_SHORTCUTS_CHANGED) {
+	    DebugStream.println("...Reason: folder shortcuts changed.");
+	    WorkspaceTreeModel.refreshFolderShortcuts();
+	}
+    }
+
+
+    private void refreshEveryNodeShowingFolder(String folder_path_str)
+    {
+	// Search through the expanded tree paths, checking if any show the given folder
+	TreePath tree_root_path = new TreePath(getModel().getRoot());
+	Enumeration expanded_tree_paths = getExpandedDescendants(tree_root_path);
+	while (expanded_tree_paths.hasMoreElements()) {
+	    TreePath expanded_tree_path = (TreePath) expanded_tree_paths.nextElement();
+	    WorkspaceTreeNode tree_node = (WorkspaceTreeNode) expanded_tree_path.getLastPathComponent();
+
+	    File tree_node_file = tree_node.getFile();
+	    if (tree_node_file == null) {
+		continue;
+	    }
+
+	    // Get the path of the open tree node
+	    String tree_node_path_str = tree_node_file.toString();
+	    if (!tree_node_path_str.endsWith(File.separator)) {
+		tree_node_path_str = tree_node_path_str + File.separator;
+	    }
+
+	    // If the specified folder is open, it must be refreshed
+	    if (tree_node_path_str.equals(folder_path_str)) {
+		//System.err.println("Must refresh node " + tree_node_path_str + "!");
+		((WorkspaceTreeModel) getModel()).refresh(expanded_tree_path);
+	    }
+	}
+    }
+
+
+    public void removeDirectoryMapping(WorkspaceTreeNode target)
+    {
+	// Update the information stored in the user's configuration
+	Configuration.removeDirectoryMapping(target.toString());
+
+	// Update tree
+	refresh(FOLDER_SHORTCUTS_CHANGED);
+    }
+
+
+    public String toString()
+    {
+	return "Workspace";
+    }
+
+
+    /** When a user right-clicks within the workspace and collection trees they are presented with a small popup menu of context based options. This class provides such functionality.
+     */
+    private class WorkspaceTreeRightClickMenu
+	extends JPopupMenu
+	implements ActionListener
+    {
+	/** The tree over which the right click action occurred. */
+	private WorkspaceTree workspace_tree = null;
+	/** The tree nodes selected when the right click action occurred. */
+	private TreePath[] selection_paths = null;
+	/** The file record over which the right click action occurred. */
+	private WorkspaceTreeNode node = null;
+
+	private JMenuItem create_shortcut = null;
+	private JMenuItem delete_shortcut = null;
+	private JMenuItem collapse_folder = null;
+	private JMenuItem expand_folder = null;
+	private JMenuItem explode_metadata_database = null;
+	private JMenuItem delete = null;
+	private JMenuItem metaaudit = null;
+	private JMenuItem new_folder = null;
+	private JMenuItem new_dummy_doc = null;
+	private JMenuItem open_externally = null;
+	private JMenuItem rename = null;
+	private JMenuItem replace = null;
+	private JMenuItem copy_collection = null;
+	private JMenuItem move_collection = null;
+
+
+	private WorkspaceTreeRightClickMenu(WorkspaceTree workspace_tree, MouseEvent event)
+	{
+	    super();
+	    this.workspace_tree = workspace_tree;
+
+	    // Note we have to use setImmediate() with the set selction paths
+	    // otherwise the selection doesn't get updated until after the 
+	    // popup comes up.
+
+	    // the right click position
+	    TreePath right_click_path = workspace_tree.getPathForLocation(event.getX(), event.getY());
+	    if (right_click_path == null) {
+		// user has clicked outside of the tree, clear the selection
+		selection_paths = null;
+		workspace_tree.setImmediate(true);
+		workspace_tree.clearSelection();
+		workspace_tree.setImmediate(false);
+	    }
+	    else {
+		// Get the paths currently selected in the tree
+		selection_paths = workspace_tree.getSelectionPaths();
+		if (selection_paths == null) {
+		    // nothing currently selected - we shift the selection to 
+		    // the node that was right clicked on
+		    selection_paths = new TreePath[1];
+		    selection_paths[0] = right_click_path;
+		    workspace_tree.setImmediate(true);
+		    workspace_tree.setSelectionPath(right_click_path);
+		    workspace_tree.setImmediate(false);
+		}
+		else if (selection_paths.length == 1 && ! selection_paths[0].equals( right_click_path)) {
+		    workspace_tree.setImmediate(true);
+		    workspace_tree.clearSelection();
+		    workspace_tree.setSelectionPath(right_click_path);
+		    workspace_tree.setImmediate(false);
+		    selection_paths[0] = right_click_path;
+		}
+		else {
+		    // we had multiply selected paths in the tree.
+		    // if we clicked on one of those paths, then use all the 
+		    // current selection, otherwise clear the selection and 
+		    // select the one we right clicked on
+		    boolean clicked_in_selection = false;
+		    for (int i = 0; i < selection_paths.length; i++) {
+			if (selection_paths[i].equals(right_click_path)) {
+			    clicked_in_selection = true;
+			    break;
+			}
+		    }
+		    if (!clicked_in_selection) {
+			// want the tree to update right away
+			workspace_tree.setImmediate(true);
+			workspace_tree.clearSelection();
+			workspace_tree.setSelectionPath(right_click_path);
+			workspace_tree.setImmediate(false);
+			selection_paths = new TreePath[1];
+			selection_paths[0] = right_click_path;
+		    }
+		}
+	    }
+	    
+	    // Create an appropriate context menu, based on what is selected
+	    buildContextMenu(selection_paths);
+
+	    // Show the popup menu on screen
+	    show(workspace_tree, event.getX(), event.getY());
+	}
+
+
+	private void buildContextMenu(TreePath[] selection_paths)
+	{
+	    // If nothing is selected, no options are available...
+	    if (selection_paths == null) {
+		return;
+	    }
+
+	    // Only meta-audit and delete are available if multiple items are selected...
+	    if (selection_paths.length > 1) {
+		return;
+	    }
+
+	    TreePath path = selection_paths[0];
+	    node = (WorkspaceTreeNode) path.getLastPathComponent();
+
+	    // ---- Options for file nodes ----
+	    if (node.isLeaf()) {
+		// Open the file in an external program
+		open_externally = new JMenuItem(Dictionary.get("Menu.Open_Externally"), KeyEvent.VK_O);
+		open_externally.addActionListener(this);
+		add(open_externally);
+
+		return;
+	    }
+
+	    // ---- Options for folder nodes ----
+	    // Collapse or expand, depending on current status
+	    if (workspace_tree.isExpanded(path)) {
+		collapse_folder = new JMenuItem(Dictionary.get("Menu.Collapse"), KeyEvent.VK_C);
+		collapse_folder.addActionListener(this);
+		add(collapse_folder);
+	    }
+	    else {
+		expand_folder = new JMenuItem(Dictionary.get("Menu.Expand"), KeyEvent.VK_O);
+		expand_folder.addActionListener(this);
+		add(expand_folder);
+	    }
+
+	    // Create/remove shortcut option, for workspace tree only
+	    if (workspace_tree == workspace_tree) {
+		// The "built-in" folders can't be modified
+		String node_name = node.toString();
+		if (node_name.equals(Dictionary.get("Tree.World")) || node_name.equals(Dictionary.get("Tree.Root")) || node_name.equals(Dictionary.get("Tree.DownloadedFiles"))) {
+		    return;
+		}
+
+		// You can unmap 1st level nodes
+		WorkspaceTreeNode root = (WorkspaceTreeNode) workspace_tree.getModel().getRoot();
+		if (root.getIndex(node) != -1) {
+		    delete_shortcut = new JMenuItem(Dictionary.get("MappingPrompt.Unmap"), KeyEvent.VK_R);
+		    delete_shortcut.addActionListener(this);
+		    add(delete_shortcut);
+		}
+		// Or map any other level directories
+		else {
+		    // --- Options for Documents In Greenstone Collections (greenstone_collections_node) ---
+		    // all subfolder of Documents In Greenstone Collections can be copied and moved
+		    WorkspaceTreeNode secondLevelNode = (WorkspaceTreeNode) path.getPathComponent(1);
+
+		    if (secondLevelNode.toString().equals(Dictionary.get("Tree.World"))) {
+			// Can move and copy the collection folders across
+			copy_collection = new JMenuItem(Dictionary.get("Menu.Copy_Collection"), KeyEvent.VK_P);
+			copy_collection.addActionListener(this);
+			add(copy_collection);
+			
+			move_collection = new JMenuItem(Dictionary.get("Menu.Move_Collection"), KeyEvent.VK_M);
+			move_collection.addActionListener(this);
+			add(move_collection);		    
+		    }
+
+		    create_shortcut = new JMenuItem(Dictionary.get("MappingPrompt.Map"), KeyEvent.VK_S);
+		    create_shortcut.addActionListener(this);
+		    add(create_shortcut);
+		}
+	    }
+	}
+
+
+	/** Called whenever one of the menu items is clicked, this method then causes the appropriate effect. */
+	public void actionPerformed(ActionEvent event)
+	{
+  	    Object source = event.getSource();
+
+	    // Create shortcut
+	    if (source == create_shortcut) {
+		//Make sure the file exists
+		if(!node.getFile().exists()){
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Folder_Does_Not_Exist"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		return;
+		}
+ 		CreateShortcutPrompt csp = new CreateShortcutPrompt(workspace_tree, node.getFile());
+ 		csp.destroy();
+	    }
+
+	    // Delete shortcut
+  	    else if (source == delete_shortcut) {
+  		workspace_tree.removeDirectoryMapping(node);
+  	    }
+
+	    // Collapse folder
+	    else if (source == collapse_folder) {
+		workspace_tree.collapsePath(selection_paths[0]);
+	    }
+
+	    // Expand folder
+	    else if (source == expand_folder) {
+		workspace_tree.expandPath(selection_paths[0]);
+	    }
+
+	    // Open in external program
+	    else if (source == open_externally) {
+		Gatherer.f_man.openFileInExternalApplication(node.getFile());
+	    }
+	    
+	    // Copy or move a collection from Documents in Greenstone Collections
+	    else if (source == move_collection || source == copy_collection) {
+
+		JFileChooser chooser = new JFileChooser(Gatherer.getCollectDirectoryPath());
+		chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);		
+		chooser.setDialogTitle(Dictionary.get("FileActions.ChooseDestinationDirectory"));
+		int returnVal = chooser.showOpenDialog(Gatherer.g_man.gather_pane);
+
+		if(returnVal == JFileChooser.APPROVE_OPTION) {		    
+
+		    // the node that the user rightclicked on ends up
+		    // being the col's import folder
+		    File sourceFolder = node.getFile(); 
+		    if(sourceFolder.getName().equals("import")) { 
+			sourceFolder = sourceFolder.getParentFile();
+		    }
+		    String target = chooser.getSelectedFile().getAbsolutePath();
+		    File targetFolder = new File(target+File.separator+sourceFolder.getName());
+		    
+		    // some sanity checks
+		    if(targetFolder.equals(sourceFolder)) { // directory has not changed. No copy/move performed
+			JOptionPane.showMessageDialog(Gatherer.g_man, 
+						      "Can't move " + sourceFolder + " " + "to itself\n(" + targetFolder + ").", 
+						      "Source and destination directories are the same", 
+						      JOptionPane.ERROR_MESSAGE);
+			return;
+		    } else if(targetFolder.exists()) { // option to overwrite or not
+			if(JOptionPane.showConfirmDialog(Gatherer.g_man, 
+							 "Directory " + targetFolder + " already exists. Overwrite?", 
+							 "Destination directory already exists", 
+							 JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION) {
+			    return;
+			}
+			// else can overwrite: delete the existing targetfolder before copying/moving
+			Utility.delete(targetFolder);
+		    } 
+		    
+		    int operation = (source == move_collection) ? FileManager.MOVE : FileManager.COPY;
+		    Gatherer.f_man.action(sourceFolder, targetFolder, operation);
+		}
+	    }
+  	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WorkspaceTreeModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WorkspaceTreeModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WorkspaceTreeModel.java	(revision 31635)
@@ -0,0 +1,173 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.file;
+
+import java.io.*;
+import java.util.*;
+import javax.swing.tree.TreePath;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.SynchronizedTreeModelTools;
+
+
+public class WorkspaceTreeModel
+    extends FileSystemModel
+{
+    static private WorkspaceTreeModel workspace_tree_model = null;
+    static private WorkspaceTreeNode workspace_tree_root = null;
+
+    static private WorkspaceTreeNode greenstone_collections_node = null;
+    static private WorkspaceTreeNode local_filespace_node = null;
+    static private WorkspaceTreeNode downloaded_files_node = null;
+    static private WorkspaceTreeNode[] folder_shortcuts = null;
+
+
+    public WorkspaceTreeModel(WorkspaceTreeNode root_node)
+    {
+	super(root_node);
+    }
+
+
+    static public WorkspaceTreeNode[] getFolderShortcuts()
+    {
+	// Return any predefined special directories
+	HashMap mappings = Configuration.getDirectoryMappings();
+	WorkspaceTreeNode[] mapping_nodes = new WorkspaceTreeNode[mappings.size()];
+	Iterator mappings_iterator = mappings.keySet().iterator();
+	for (int i = 0; mappings_iterator.hasNext(); i++) {
+	    String mapping_name = (String) mappings_iterator.next();
+	    File mapping_file = (File) mappings.get(mapping_name);
+	    mapping_nodes[i] = new WorkspaceTreeNode(mapping_file, mapping_name);
+	}
+	return mapping_nodes;
+    }
+
+
+    static public WorkspaceTreeModel getWorkspaceTreeModel()
+    {
+	// Create a root node to contain the various nodes in the workspace tree
+ 	workspace_tree_root = new WorkspaceTreeNode(null, "ABS_ROOT");
+	workspace_tree_model = new WorkspaceTreeModel(workspace_tree_root);
+
+	// Add the "Documents in Greenstone Collections" node
+	greenstone_collections_node = new WorkspaceTreeNode(null, Dictionary.get("Tree.World"));
+	workspace_tree_root.add(greenstone_collections_node);
+
+	// Add the "Local Filespace" node
+	local_filespace_node = FileSystem.getLocalFilespaceNode(workspace_tree_model);
+	workspace_tree_root.add(local_filespace_node);
+
+	// Add the "Home Folder" node
+	workspace_tree_root.add(FileSystem.getHomeFolderNode());
+
+	// Add the "Downloaded Files" node, if the Download pane is enabled
+	if (Gatherer.g_man.download_pane != null) {
+	    downloaded_files_node = FileSystem.getDownloadedFilesNode();
+	    workspace_tree_root.add(downloaded_files_node);
+	}
+
+	// Add any folder shortcuts the user has created
+	refreshFolderShortcuts();
+
+ 	return workspace_tree_model;
+    }
+
+
+    static public void refreshGreenstoneCollectionsNode()
+    {
+	greenstone_collections_node.refresh();
+    }
+
+
+    static public void refreshDownloadedFilesNode()
+    {
+	downloaded_files_node.refresh();
+    }
+
+
+    static public void refreshFolderShortcuts()
+    {
+	// Check for new/deleted folder shortcuts
+	WorkspaceTreeNode[] old_folder_shortcuts = folder_shortcuts;
+	folder_shortcuts = getFolderShortcuts();
+
+	// Remove any deleted shortcuts from the tree
+	if (old_folder_shortcuts != null) {
+	    for (int i = 0; i < old_folder_shortcuts.length; i++) {
+		if (!doesArrayContain(folder_shortcuts, old_folder_shortcuts[i])) {
+		    DebugStream.println("Deleted shortcut: " + old_folder_shortcuts[i]);
+		    SynchronizedTreeModelTools.removeNodeFromParent(workspace_tree_model, old_folder_shortcuts[i]);
+		}
+	    }
+	}
+
+	// Add any new shortcuts to the tree
+	if (folder_shortcuts != null) {
+	    for (int i = 0; i < folder_shortcuts.length; i++) {
+		if (!doesArrayContain(old_folder_shortcuts, folder_shortcuts[i])) {
+		    DebugStream.println("Added shortcut: " + folder_shortcuts[i]);
+		    SynchronizedTreeModelTools.insertNodeInto(workspace_tree_model, workspace_tree_root, folder_shortcuts[i], false);
+		}
+	    }
+	}
+    }
+
+
+    // -- This code is necessary to support the workspace tree file filter --
+    public void refresh(TreePath path)
+    {
+	// If we're not refreshing the whole tree just refresh a certain path
+	if (path != null) {
+	    super.refresh(path);
+	    return;
+	}
+
+	// Refresh each of the nodes in the workspace tree
+	for (int i = 0; i < workspace_tree_root.getChildCount(); i++) {
+	    WorkspaceTreeNode child_node = (WorkspaceTreeNode) workspace_tree_root.getChildAt(i);
+	    super.refresh(new TreePath(child_node.getPath()));
+	}
+    }
+
+
+    static private boolean doesArrayContain(Object[] array, Object item)
+    {
+	if (array == null) {
+	    return false;
+	}
+
+	for (int i = 0; i < array.length; i++) {
+	    if (item.toString().equals(array[i].toString())) {
+		return true;
+	    }
+	}
+
+	return false;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WorkspaceTreeNode.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WorkspaceTreeNode.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WorkspaceTreeNode.java	(revision 31635)
@@ -0,0 +1,214 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.file;
+
+
+import java.io.*;
+import java.util.*;
+import javax.swing.filechooser.FileSystemView;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.BasicCollectionConfiguration;  // !!! Don't like this here
+import org.greenstone.gatherer.collection.CollectionManager;  // !!! Don't like this here
+import org.greenstone.gatherer.util.ArrayTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+
+/** This class represents one node in the workspace tree. */
+public class WorkspaceTreeNode
+    extends FileNode
+{
+    private boolean is_gs3_site_node = false;
+    private boolean is_group_node = false;
+    private boolean is_in_loaded_collection = false;
+    private String title = null;
+
+
+    public WorkspaceTreeNode(File file)
+    {
+	super(file);
+    }
+
+
+    public WorkspaceTreeNode(File file, String title)
+    {
+	this(file);
+	this.title = title;
+    }
+
+
+    public FileNode addChildNode(File file)
+    {
+	WorkspaceTreeNode child_node = new WorkspaceTreeNode(file);
+	child_node.setModel(model);
+	child_node.setParent(this);
+	return child_node;
+    }
+
+
+    /** Is this file node within the currently loaded collection? */
+    public boolean isInLoadedCollection()
+    {
+	if (is_in_loaded_collection) {
+	    return true;
+	}
+	else {
+	    FileNode parent = (FileNode) getParent();
+	    if (parent != null) {
+		return parent.isInLoadedCollection();
+	    }
+	}
+	return false;
+    }
+
+
+    public boolean isReadOnly()
+    {
+	// The workspace tree is read only
+	return true;
+    }
+
+
+    public void map()
+    {
+	// Special Case: "Documents in Greenstone Collections" -- map the collections installed in Greenstone
+	if (file == null) { // a special mapping folder
+	    if (child_nodes != null) {
+		// don't bother mapping again if we already have children
+		return;
+	    }
+	    child_nodes = new ArrayList();
+	    if (title.equals(Dictionary.get("Tree.World")) && Gatherer.GS3) {
+		// the Greenstone collections folder for GS3 - this contains a 
+		// folder for each site
+
+		File start = new File(Utility.getSitesDir(Configuration.gsdl3_path));
+		File sites[] = start.listFiles();
+		ArrayTools.sort(sites);
+		for (int i = 0; sites != null && i < sites.length; i++) {
+		    File collect_dir = new File(sites[i], "collect");
+		    if (!collect_dir.exists()) {
+			continue;
+		    }
+
+		    WorkspaceTreeNode child = new WorkspaceTreeNode(null, sites[i].getName());
+		    child.is_gs3_site_node = true;
+		    child.unmap();
+		    child.setModel(model);
+		    child.setParent(this);
+		    child.map();
+		    child_nodes.add(child);
+		}
+		model.nodeStructureChanged(this);
+		
+	    } else if (title.equals(Dictionary.get("Tree.World")) || is_gs3_site_node || is_group_node) {
+		// For each of the children directories, which are collections...
+		File start;
+		if (is_gs3_site_node ) {
+		    start = new File(Gatherer.getSitesDirectoryPath() + title + File.separator + "collect" + File.separator);
+		} 
+		else if (is_group_node) {
+		  start = new File(Gatherer.getCollectDirectoryPath() + title);
+		}
+		else {
+		    start = new File(Gatherer.getCollectDirectoryPath());
+		}
+		File cols[] = start.listFiles();
+		ArrayTools.sort(cols);
+
+		// We add their import directories, except for the model collection
+		for (int i = 0; cols != null && i < cols.length; i++) {
+		    if (!cols[i].getName().equals(StaticStrings.MODEL_COLLECTION_NAME)) {
+			// check for groups
+			String file_name = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+			BasicCollectionConfiguration collect_cfg = new BasicCollectionConfiguration(new File(cols[i], file_name));
+			if (collect_cfg.getCollectGroup().equals("true")) {
+			  WorkspaceTreeNode child = new WorkspaceTreeNode(null, cols[i].getName());
+			  child.is_group_node = true;
+			  child.unmap();
+			  child.setModel(model);
+			  child.setParent(this);
+			  child.map();
+			  child_nodes.add(child);
+			} else {
+			  File import_dir = new File(cols[i], "import");
+			  if (!import_dir.exists()) {
+			    continue;
+			  }
+			  
+			  WorkspaceTreeNode collection_root = new WorkspaceTreeNode(import_dir, collect_cfg.toString());
+			  collection_root.setParent(this);
+			  collection_root.setModel(model);
+			  
+			  // One last piece of magic so we can determine the current collection
+			  // Now made to work with collection groups
+			  String groupQualifiedColName = cols[i].getName();
+			  if(is_group_node) { // then title would have stored the colgroup name
+			      groupQualifiedColName = title + File.separator + cols[i].getName();
+			  }
+			  collection_root.is_in_loaded_collection = groupQualifiedColName.equals(CollectionManager.getLoadedCollectionName());
+			  child_nodes.add(collection_root);
+			}
+		    }
+		}
+		model.nodeStructureChanged(this);
+	    }
+	    else if (title.equals(Dictionary.get("Tree.Root"))) {
+		// Special Case: "Local Filespace" on Windows -- create a node for each filesystem root (drive letter)
+		
+		// Sort the roots into alphabetical order
+		File[] roots = File.listRoots();
+		ArrayTools.sort(roots);
+		for (int i = 0; i < roots.length; i++) {
+		    // Only add root if it isn't a floppy drive
+		    // this used to cause problems, I don't think it does now...
+		    //if (!FileSystemView.getFileSystemView().isFloppyDrive(roots[i])) {
+		    child_nodes.add(addChildNode(roots[i]));
+			// }
+		}
+	    }
+	} // if file == null
+	
+	// General case
+	else {
+	    super.map();
+	}
+    }
+
+
+    public String toString()
+    {
+	if (title != null) {
+	    return title;
+	}
+
+	return super.toString();
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WriteNotPermittedException.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WriteNotPermittedException.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/file/WriteNotPermittedException.java	(revision 31635)
@@ -0,0 +1,11 @@
+package org.greenstone.gatherer.file;
+
+import java.lang.Exception;
+
+public class WriteNotPermittedException
+    extends Exception {
+
+    public WriteNotPermittedException(String message) {
+	super(message);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/Attribute.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/Attribute.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/Attribute.java	(revision 31635)
@@ -0,0 +1,122 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+
+/**
+ *  Wrapper class for attributes displayed in the table 
+ * @author Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ */
+public class Attribute
+{
+    private String name="";
+    private String value="";
+    private String lang="";
+    private boolean isRequired;
+    //private boolean isLanguageDependent;
+
+    public Attribute() {}
+
+    public Attribute(String name, String value, String lang, boolean isRequired /*, boolean isLangDep*/){
+	this.name = name;
+	this.value = value;
+	this.lang = lang;
+	this.isRequired = isRequired;
+	//this.isLanguageDependent = isLangDep;
+    }
+
+    public Attribute(String name, String value, boolean isRequired/*, boolean isLangDep*/){
+	this.name = name;
+	this.value = value;
+	//this.lang = lang;
+	this.isRequired = isRequired;
+	//this.isLanguageDependent = isLangDep;
+    }
+
+    public Attribute(String name, String value){
+	this.name = name;
+	this.value = value;
+    }
+
+    public String getName(){
+	return name;
+    }
+
+    public String getValue() {
+	return value;
+    }
+
+    public String getLanguage() {
+	return lang;
+    }
+  
+    public boolean isRequired(){
+	return this.isRequired;
+    }
+
+//     public boolean isLanguageDependent(){
+// 	return this.isLanguageDependent;
+//     }
+       
+    public void setName(String name) {
+	if (name.trim().equals(this.name)) return;
+	this.name = name.trim();
+        
+    }
+    
+    public void setValue(String value) {
+	if (value.trim().equals(this.value)) return;
+	this.value = value.trim();
+    }
+
+    public void setLanguage(String lang) {
+	if (lang.trim().equals(this.lang)) return;
+	this.lang = lang;
+    }
+
+    public void setRequired(boolean isRequired) {
+	this.isRequired = isRequired;
+    } 
+    
+//     public void setLanguageDependent(boolean isLangDep) {
+// 	this.isLanguageDependent = isLangDep;
+//     }
+
+    public String toString() {
+	return (this.name + " " + this.value +" " + this.lang);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeContainer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeContainer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeContainer.java	(revision 31635)
@@ -0,0 +1,183 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Katherine Don, New Zealand Digital Library Project, 
+ *          University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import org.greenstone.gatherer.Configuration;
+
+import java.util.ArrayList;
+
+
+/** this is used as a base class for MetadataSetInfo and MetadataElementModel
+ * Both of thesee have some attributes which are language independent, and
+ * some which are language dependent
+ */ 
+public class AttributeContainer {
+    
+    protected ArrayList attributes;     
+    protected ArrayList language_dependent_attributes;    
+    
+    String [] required_attribute_names = null;
+    String [] language_dependent_attribute_names = null;
+    
+    public AttributeContainer() {}
+    public AttributeContainer(String [] required_atts, String []lang_dependent_atts) {       
+	
+	attributes = new ArrayList();
+	language_dependent_attributes =  new ArrayList();               
+
+	if (required_atts != null) {
+	    required_attribute_names = required_atts;
+	    for(int i=0;i<required_atts.length;i++){
+		String reName = required_atts[i];
+		Attribute attr = new Attribute(reName,"",true);
+		attributes.add(attr);            
+	    }
+	}
+
+	if (lang_dependent_atts != null) {
+	    language_dependent_attribute_names = lang_dependent_atts;
+	    for(int i=0;i<lang_dependent_atts.length;i++){
+		String reName = lang_dependent_atts[i];
+		Attribute attr = new Attribute(reName,"",Configuration.getLanguage(),true);
+		language_dependent_attributes.add(attr);            
+	    }
+	}
+	
+    }
+
+    public ArrayList getAttributes(){
+	return attributes;
+    }
+    
+    public void setAttributes(ArrayList atts) {
+	attributes.clear();
+	attributes = atts;
+    }
+
+    public String [] getLanguageDependentAttributeNames() {
+	return language_dependent_attribute_names;
+    }
+    public ArrayList getLanguageDependentAttributes(){
+	return language_dependent_attributes; 
+    }
+    
+    public void setLanguageDependentAttributes(ArrayList lda){
+	language_dependent_attributes.clear();
+	language_dependent_attributes = lda;
+    }
+
+
+    protected ArrayList getAllLanguages(){
+	ArrayList langs = new ArrayList();    
+
+       for(int i=0;i<language_dependent_attributes.size();i++){
+	    Attribute attr = (Attribute) language_dependent_attributes.get(i);
+            if (!langs.contains(attr.getLanguage()) && !attr.getLanguage().trim().equals("")){
+		langs.add(attr.getLanguage());
+	    } 
+	}   
+
+       return langs;
+    }
+
+    public int languageExists(String lang){
+	for(int i=0;i<language_dependent_attributes.size();i++){
+	    Attribute attr = (Attribute) language_dependent_attributes.get(i);
+	    if (attr.getLanguage().trim().equals(lang.trim())){
+		return i;
+	    } 
+	}   
+	
+	return -1;
+    }
+
+    protected Attribute getAttributeByName(String name){
+        for(int i=0;i<attributes.size();i++){
+	    Attribute attr = (Attribute) attributes.get(i);
+            if (attr.getName() != null && attr.getName().trim().equals(name.trim())){
+		return attr;
+	    } 
+	}
+
+	return null;
+    }
+
+    protected Attribute getAttributeByNameAndLanguage(String name, String lang){
+	for(int i=0;i<language_dependent_attributes.size();i++){
+	    Attribute attr = (Attribute) language_dependent_attributes.get(i);
+	    if (attr.getName() != null || attr.getLanguage()!=null){
+		if (attr.getName().trim().equals(name.trim()) && (attr.getLanguage().trim().equals(lang.trim()))){
+		    return attr;
+		}
+	    } 
+	}
+	
+	return null;
+    
+    }
+
+    protected ArrayList getAttributesByLanguage(String lang){
+	  ArrayList attrs = new ArrayList();    
+	  
+	  for(int i=0;i<language_dependent_attributes.size();i++){
+	      Attribute attr = (Attribute) language_dependent_attributes.get(i);
+	      if (!attr.getLanguage().trim().equals("") && attr.getLanguage().trim().equals(lang)){
+		  attrs.add(attr);
+	      } 
+	  }   
+	  
+	  return attrs;
+      }
+
+   protected boolean isAttributeRequired(String name) {
+       for(int i=0;i<required_attribute_names.length;i++){
+            if (name.equals(required_attribute_names[i])) return true;
+	}
+	return false;
+
+    }
+
+    protected boolean isLangDependentAttributeRequired(String name) {
+	for(int i=0;i<language_dependent_attribute_names.length;i++){
+            if (name.equals(language_dependent_attribute_names[i])) return true;
+	}
+	return false;
+
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeEvent.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeEvent.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeEvent.java	(revision 31635)
@@ -0,0 +1,52 @@
+
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at th * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################## ##########################
+ */
+package org.greenstone.gatherer.gems;
+
+public class AttributeEvent{
+    Attribute attribute;
+
+    public AttributeEvent(Attribute attr){
+        attribute = attr;
+
+    }
+
+     public Attribute getSource(){
+	 return attribute;
+    }
+
+    //add get and set method 
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeListener.java	(revision 31635)
@@ -0,0 +1,43 @@
+
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at th * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################## ##########################
+ */
+package org.greenstone.gatherer.gems;
+
+public interface AttributeListener{
+
+    public void attributeChanged(AttributeEvent ae);
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeTable.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeTable.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/AttributeTable.java	(revision 31635)
@@ -0,0 +1,325 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import javax.swing.table.*; 
+import javax.swing.JTable; 
+import java.awt.event.ActionListener;
+import java.awt.event.ActionEvent;
+import java.util.ArrayList; 
+import java.awt.Rectangle;
+import javax.swing.JOptionPane;
+import javax.swing.DefaultCellEditor;
+import javax.swing.JTextField;
+import javax.swing.ListSelectionModel;
+
+import org.greenstone.gatherer.Dictionary;
+
+public class AttributeTable
+    extends JTable 
+    implements MetadataElementListener, MetadataSetListener, ActionListener {
+
+    // self reference
+    JTable this_table; 
+    
+    private ArrayList listeners = new ArrayList();
+    private JTextField metadata_value_text_field = new JTextField();
+    private boolean language_dependent;
+    
+    public AttributeTable(boolean lang_dependent){
+	super();       
+        setRowHeight(20);
+        this.setComponentOrientation(Dictionary.getOrientation());
+        metadata_value_text_field.setComponentOrientation(Dictionary.getOrientation());
+	language_dependent = lang_dependent;
+	
+    	metadata_value_text_field.setBorder(null);
+	DefaultCellEditor cellEditor = new DefaultCellEditor(metadata_value_text_field);
+	cellEditor.setClickCountToStart(1);
+	setDefaultEditor(String.class,cellEditor);   
+        this_table = this;
+	setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+      }
+
+    /* not currently used */
+    public void actionPerformed(ActionEvent e){
+	String command = e.getActionCommand();
+   
+        if (command.equals(GEMSConstants.ADD_ATTRIBUTE)){
+	    addAttribute();
+ 	    return ;
+	}
+       
+       if (command.equals(GEMSConstants.DELETE_ATTRIBUTE)){
+           deleteAttribute();
+	}   
+           
+    }
+
+    public void addNewLanguage(String lang) {
+	AttributeTableModel model = (AttributeTableModel)getModel();
+	model.addNewLanguage(lang);
+    }
+
+    public void metadataElementChanged(MetadataElementEvent mee){
+	MetadataElementModel model = mee.getMetadataElementModel();      
+        if (model == null){ //this model has been deleted
+	    setModel(new DefaultTableModel());
+            return;
+	} 
+	populateAttributeTable(model);
+    }
+
+    public void metadataSetChanged(MetadataSetEvent mse){
+	MetadataSetInfo info = mse.getMetadataSetInfo();
+	if (info == null){
+	    setModel(new DefaultTableModel());
+	    return;
+	}
+        populateAttributeTable(info);
+    }
+    
+    public void addAttributeListener(AttributeListener al){
+	if (!listeners.contains(al)){
+	    listeners.add(al);    
+	}
+    }
+
+    public void removeAttributeListener(AttributeListener al){
+	listeners.remove(al);   
+    }
+
+    private void addAttribute(){
+	AttributeTableModel model = (AttributeTableModel)getModel();
+	model.addNewRow();
+    }
+
+   
+    private void deleteAttribute(){
+	int index = getSelectedRow();
+	AttributeTableModel model = (AttributeTableModel)getModel();
+	model.deleteRow(index);
+    }
+    
+    private void populateAttributeTable(AttributeContainer container){
+	AttributeTableModel model = new AttributeTableModel(container, language_dependent);
+	setModel(model);
+	TableColumnModel tcm = getColumnModel();
+	if (language_dependent) {
+	    tcm.getColumn(0).setPreferredWidth(100);
+	    tcm.getColumn(1).setPreferredWidth(50);
+	    tcm.getColumn(2).setPreferredWidth(350);         
+	}
+	else {
+	    tcm.getColumn(0).setPreferredWidth(100);
+	    tcm.getColumn(1).setPreferredWidth(400);         
+	} 
+    }
+
+
+
+    private void notifyListeners(Attribute attr){
+	AttributeEvent ae = new AttributeEvent(attr);
+	for(int i=0;i<listeners.size();i++){
+	    AttributeListener al = (AttributeListener) listeners.get(i);
+	    al.attributeChanged(ae);
+	    
+	}
+    }
+
+
+
+    protected class AttributeTableModel extends AbstractTableModel {
+    
+	AttributeContainer attribute_container;
+	ArrayList attributes;
+	boolean language_dependent;
+    
+	String[] column_names;
+
+	public AttributeTableModel(AttributeContainer container, boolean lang_dependent) {
+	    attribute_container = container;
+	    language_dependent = lang_dependent;
+	    if (language_dependent) {
+		attributes = container.getLanguageDependentAttributes();
+		column_names = GEMSConstants.LANG_DEPENDENT_ATTRIBUTES_COLUMN_NAMES;
+	    } 
+	    else {
+		attributes = container.getAttributes();
+		column_names = GEMSConstants.ATTRIBUTES_COLUMN_NAMES;
+	    }
+	}
+
+	public int getRowCount(){
+	    return attributes.size();
+	}
+       
+	public String getColumnName(int column){
+	    return column_names[column];
+	}
+	
+	public Class getColumnClass(int columnIndex){
+	    return String.class;
+	}    
+	public int getColumnCount(){
+	    return column_names.length;
+	}
+	public boolean isCellEditable(int row, int col) {
+	    Attribute attr = (Attribute)attributes.get(row);	   
+	    if (col == 0 && attr.isRequired()) return false;
+	    return true;
+	}
+
+	public Object getValueAt(int row, int column){
+	    if (row < attributes.size()){
+		Attribute attr = (Attribute)attributes.get(row);
+		// 	String name = attr.getName();
+		//  if (attr.isRequired()){
+		//    name = "<html><body Color=blue>"+name+"</body></html>";
+		// 	}
+
+		if (column == 0) return attr.getName();
+		if (language_dependent) {
+		    if (column == 1) return attr.getLanguage();
+		    if (column == 2) return attr.getValue();
+		}
+		else {
+		    if (column == 1) return attr.getValue();
+		}
+	    }	
+	    return null; 
+	}	    	    
+
+	public void setValueAt(Object value, int row, int column){    
+	    if (row < attributes.size() && attributes.size()>0 ){
+		Attribute attr = (Attribute)attributes.get(row);
+		if (attr.isRequired() && ((String)value).trim().equals("") && !getValueAt(row,column).equals("")) {
+		    JOptionPane.showMessageDialog(null,Dictionary.get("GEMS.Attribute_Edition_Error_Message"), Dictionary.get("GEMS.Attribute_Edition_Error"),JOptionPane.ERROR_MESSAGE);
+		    return; 
+		}
+		
+		// TODO: check namespace conflict
+		if (column == 0) attr.setName((String)value);
+		else {
+		    if (language_dependent) {
+			if (column == 1) attr.setLanguage((String)value);
+			else if (column == 2) attr.setValue((String)value); 
+		    }
+		    else {
+			if (column == 1) attr.setValue((String)value);
+		    }
+		}
+		notifyListeners(attr);
+
+		/// check this stuff
+		if (attribute_container instanceof MetadataElementModel) {
+		    if (attr.getName().trim().equals(GEMSConstants.NAME_ATTRIBUTE)) {
+			((MetadataElementModel)attribute_container).setName((String)value);
+		    }
+		} else if (attribute_container instanceof MetadataSetInfo) {
+		    ((MetadataSetInfo)attribute_container).infoChanged(); 
+		}
+	    }
+	}
+
+	/** used only for language dependent attribute tables */
+	public void addNewLanguage(String lang)
+	{
+	    if (!language_dependent) return;
+	    int index = attribute_container.languageExists(lang);  
+	    if (index >=0){
+		this_table.getSelectionModel().setSelectionInterval(index,index);
+		Rectangle rect = this_table.getCellRect(index, 0, true);
+		this_table.scrollRectToVisible(rect);
+	    }
+	    else {
+		String [] attrNames = attribute_container.getLanguageDependentAttributeNames();
+		if (attrNames == null){
+		    JOptionPane.showMessageDialog(null,Dictionary.get("GEMS.Add_Lang_Depend_Attr_Error_Message"),Dictionary.get("GEMS.Add_Lang_Depend_Attr_Error"),JOptionPane.ERROR_MESSAGE);
+		    return ;
+		}
+		for(int i=0;i<attrNames.length;i++){
+		    Attribute attr = new Attribute(attrNames[i].trim(),"", true);
+		    attr.setLanguage(lang);
+		    attributes.add(attr); 
+		    notifyListeners(attr);
+		}
+		fireTableDataChanged();
+		// first att from new lot
+		int first_new_att_index = attributes.size()-attrNames.length;
+		this_table.editCellAt(first_new_att_index, 2);
+		if (this_table.isEditing()) {
+		    this_table.getEditorComponent().requestFocus();
+		}
+	    
+		this_table.getSelectionModel().setSelectionInterval(attributes.size()-attrNames.length,attributes.size()-1); 
+		Rectangle rect = this_table.getCellRect(attributes.size()-1, 0, true);
+		this_table.scrollRectToVisible(rect); 
+	    }
+	}
+    
+	/* not currently used */
+	public void addNewRow(){
+	    Attribute attr = new Attribute();
+	    attributes.add(attr);
+	    fireTableDataChanged(); 
+	    this_table.getSelectionModel().setSelectionInterval(attributes.size()-1,attributes.size()-1);
+	    Rectangle rect = this_table.getCellRect(attributes.size()-1, 0, true);
+	    this_table.scrollRectToVisible(rect);    
+	}
+
+	/* not currently used */
+	public void deleteRow(int index){           
+	    if (index < attributes.size() && index >=0){
+		Attribute attr = (Attribute)attributes.get(index);          
+		if (attr.isRequired()){
+		    JOptionPane.showMessageDialog(null,Dictionary.get("GEMS.Attribute_Deletion_Error_Message"), Dictionary.get("GEMS.Attribute_Deletion_Error"),JOptionPane.ERROR_MESSAGE);
+		    return;
+		}
+
+		// 		int result = JOptionPane.showOptionDialog(null, Dictionary.get("GEMS.Confirm_Removal", Dictionary.get("GEMS.Attribute") + " " +  Dictionary.get("GEMS.Cannot_Undo")), Dictionary.get("GEMS.Confirm_Removal_Title"), JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE,null,GEMSConstants.DIALOG_OPTIONS,GEMSConstants.DIALOG_OPTIONS[0] );
+         	
+		//if (result !=JOptionPane.OK_OPTION) return;
+		notifyListeners(attr);
+		attributes.remove(index);
+		fireTableDataChanged();  
+	    }
+	
+	}
+    
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/DeleteMetadataSetPrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/DeleteMetadataSetPrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/DeleteMetadataSetPrompt.java	(revision 31635)
@@ -0,0 +1,281 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Vector;
+import java.util.HashMap;
+
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.gui.ModalDialog;
+import org.greenstone.gatherer.gui.GLIButton;
+
+public class DeleteMetadataSetPrompt 
+    extends ModalDialog { 
+
+    static private Dimension SIZE = new Dimension(500, 500);
+        
+    private ArrayList available_metadata_sets;
+    private ArrayList listeners;
+
+    private JCheckBox confirmation = null;
+    private JButton delete_button = null;
+    private JButton close_button = null;    
+    private JList available_set_list = null;
+    /** The model behind the list. */
+    private DefaultListModel list_model = null;
+    private JTextArea description_textarea = null;
+    private DeleteMetadataSetPrompt self;
+    private MetadataSetManager meta_manager;
+  
+    public DeleteMetadataSetPrompt(Frame parent,MetadataSetManager msm) {
+	super(parent, true);
+        self = this;
+	meta_manager = msm;
+        listeners = new ArrayList();      
+
+	setSize(SIZE);
+	setTitle(Dictionary.get("GEMS.DeleteMetadataSetPrompt.Title"));
+   
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setOpaque(true);
+	content_pane.setComponentOrientation(Dictionary.getOrientation());
+
+	JLabel available_metadata_sets_label = new JLabel(Dictionary.get("GEMS.DeleteMetadataSetPrompt.Available_Sets"));
+	available_metadata_sets_label.setOpaque(true);
+        available_metadata_sets_label.setComponentOrientation(Dictionary.getOrientation());
+	
+	list_model = new DefaultListModel();
+	
+        available_set_list = new JList();
+	available_set_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+	available_set_list.setModel(list_model);
+	available_set_list.setCellRenderer(new MetadatSetListCellRenderer()); 
+	available_set_list.setFixedCellHeight(20);
+	available_set_list.addListSelectionListener(new MetadataSetListSelectionListener());
+        available_set_list.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel set_pane = new JPanel();
+        set_pane.setComponentOrientation(Dictionary.getOrientation());
+        set_pane.setLayout(new BorderLayout());
+        set_pane.add(available_metadata_sets_label,BorderLayout.NORTH);
+	set_pane.add(new JScrollPane(available_set_list),BorderLayout.CENTER);
+
+
+	JLabel metadata_set_des_label = new JLabel(Dictionary.get("GEMS.Set_Description"));
+	metadata_set_des_label.setOpaque(true);
+
+	description_textarea = new JTextArea();
+        description_textarea.setOpaque(true);
+        description_textarea.setEditable(false);
+        description_textarea.setLineWrap(true); 
+        description_textarea.setWrapStyleWord(true);
+	description_textarea.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel des_pane = new JPanel();
+        des_pane.setComponentOrientation(Dictionary.getOrientation());
+        des_pane.setLayout(new BorderLayout());
+        des_pane.add(metadata_set_des_label,BorderLayout.NORTH);
+	des_pane.add(new JScrollPane(description_textarea),BorderLayout.CENTER);
+	
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	delete_button = new GLIButton(Dictionary.get("GEMS.DeleteMetadataSetPrompt.Delete"), Dictionary.get("GEMS.DeleteMetadataSetPrompt.Delete_Tooltip"));
+	delete_button.setEnabled(false);
+	
+
+	confirmation = new JCheckBox(Dictionary.get("GEMS.DeleteMetadataSetPrompt.Confirm_Delete"));
+	confirmation.setEnabled(false);
+	confirmation.setSelected(false);
+        confirmation.setComponentOrientation(Dictionary.getOrientation());
+        
+	close_button = new GLIButton(Dictionary.get("General.Close"), Dictionary.get("General.Close_Tooltip"));
+	close_button.setEnabled(true);
+
+	confirmation.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    delete_button.setEnabled(confirmation.isSelected());
+		    //confirmation.setEnabled(false);
+		    //confirmation.setSelected(false);
+		}
+	    });
+
+	// Add listeners
+	delete_button.addActionListener(new DeleteButtonListener());
+	
+	close_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    self.dispose();
+		}
+	    });
+
+       
+	button_pane.setLayout(new GridLayout(1,2));
+	button_pane.add(delete_button);
+	button_pane.add(close_button);
+
+        JPanel bottom_pane = new JPanel();
+        bottom_pane.setComponentOrientation(Dictionary.getOrientation());
+        bottom_pane.setLayout(new BorderLayout());
+	bottom_pane.add(confirmation,BorderLayout.NORTH);
+	bottom_pane.add(button_pane, BorderLayout.CENTER);
+
+
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(set_pane, BorderLayout.NORTH);
+	content_pane.add(des_pane, BorderLayout.CENTER);
+	content_pane.add(bottom_pane, BorderLayout.SOUTH);
+	
+	// Show
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(false);
+    }
+
+      
+    public void display() {
+	available_metadata_sets = meta_manager.getAvailableMetadataSets();
+	for (int i=0; i<available_metadata_sets.size(); i++) {
+	    list_model.addElement(available_metadata_sets.get(i));
+	}
+	//available_set_list.setListData(new Vector((Collection)available_metadata_sets));
+	delete_button.setEnabled(false);
+	confirmation.setEnabled(false);
+	confirmation.setSelected(false);
+       	setVisible(true); 
+        
+    }
+
+
+    /** The Delete button listener implementation. */
+    private class DeleteButtonListener 
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so we can be informed when the button is actioned.
+	 * @param event An <strong>ActionEvent</strong> containing all the relevant information garnered from the event itself.
+	 * @see org.greenstone.gatherer.Configuration
+	 * @see org.greenstone.gatherer.Gatherer
+	 * @see org.greenstone.gatherer.util.Utility
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    // Delete the selected MetadataSet.
+	    Object selectedValue = available_set_list.getSelectedValue();
+		
+	    if (selectedValue !=null && (selectedValue instanceof MetadataSetInfo)){
+		((MetadataSetInfo)selectedValue).deleteMetadataSet();
+		list_model.removeElement(selectedValue);
+	    }
+	    delete_button.setEnabled(false);
+	    confirmation.setEnabled(false);
+	    confirmation.setSelected(false);
+	    description_textarea.setText(Dictionary.get("GEMS.DeleteMetadataSetPrompt.No_Set"));
+	    //self.dispose();
+	}
+
+    }
+
+
+    private class MetadatSetListCellRenderer extends JLabel implements ListCellRenderer {
+	public MetadatSetListCellRenderer() {
+	    setOpaque(true);
+            this.setComponentOrientation(Dictionary.getOrientation());
+	}
+	
+	public Component getListCellRendererComponent(JList list,
+						      Object value,
+						      int index,
+						      boolean isSelected,
+						      boolean cellHasFocus)
+	{
+	    String name= "unknown";
+
+	    if (value instanceof MetadataSetInfo){
+		MetadataSetInfo meta_info = (MetadataSetInfo) value;
+		name = meta_info.getMetadataSetName(); 
+	    }
+  
+            setText(name);
+            if (isSelected) {
+		setBackground(list.getSelectionBackground());
+		setForeground(list.getSelectionForeground());
+	    }
+	    else {
+		setBackground(list.getBackground());
+		setForeground(list.getForeground());
+	    }
+	              
+	    return this;
+	}
+    }
+
+   
+    private class MetadataSetListSelectionListener implements ListSelectionListener  {
+        public void valueChanged(ListSelectionEvent lse){
+	    if (lse.getValueIsAdjusting()) return;
+	    
+	    delete_button.setEnabled(false);
+	   
+	    Object selectedValue = available_set_list.getSelectedValue();
+
+            if (selectedValue !=null && (selectedValue instanceof MetadataSetInfo)){
+                MetadataSetInfo meta_info = (MetadataSetInfo)selectedValue; 
+		description_textarea.setText(meta_info.getMetadataSetDescription());
+		confirmation.setEnabled(true);
+		confirmation.setSelected(false);
+	    }
+	    else {
+		confirmation.setEnabled(false);
+		description_textarea.setText(Dictionary.get("GEMS.DeleteMetadataSetPrompt.No_Set"));
+	    }      
+
+	}
+    }
+    
+
+    
+    public void addMetadataSetListener(MetadataSetListener msl){
+        listeners.add(msl);  
+    }
+
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/GEMS.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/GEMS.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/GEMS.java	(revision 31635)
@@ -0,0 +1,591 @@
+ /**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at th * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################## ##########################
+ */
+package org.greenstone.gatherer.gems;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import java.lang.String;
+import javax.swing.event.*;
+import javax.swing.filechooser.*;
+import javax.swing.text.*;
+import javax.swing.tree.*;
+import java.awt.*;
+import java.awt.event.*;
+
+import java.io.File;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.GetOpt;
+import org.greenstone.gatherer.gui.GComboBox;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.HelpFrame;
+import org.greenstone.gatherer.gui.ModalDialog;
+import org.greenstone.gatherer.gui.NonWhitespaceField;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.greenstone.gatherer.Dictionary;
+import org.w3c.dom.*;
+
+
+/* 
+ * @author Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */ 
+public class GEMS
+    extends JFrame implements WindowFocusListener 
+{
+ 
+    static final private Dimension SIZE = new Dimension(800,550);
+
+    // we have a card pane to switch between the no set loaded view, and the 
+    // set loaded view
+    private CardLayout card_layout = null;
+    private JPanel card_pane = null;
+    /** The name of the panel containing the "set loaded" card. */
+    private String SET_LOADED_CARD = "";
+    /** The name of the panel containing the "no set loaded" placeholder */
+    private String NO_SET_LOADED_CARD = "No set loaded";
+
+    private GEMS self = null;
+
+    private OpenMetadataSetPrompt open_prompt;
+    private DeleteMetadataSetPrompt delete_prompt;
+    private NewMetadataSetPrompt new_prompt;   
+    
+    // this is the main panel that gets switched in once a set is loaded
+    private JSplitPane metadata_set_details_split_pane = null;
+    // this is the split with the attribute tables in it
+    private JSplitPane attribute_tables_split_pane = null;
+     
+    /** Um, the size of the screen I'd guess. */
+    private Dimension screen_size = null;
+    
+    private MetadataSetManager msm;
+
+    private AttributeTable attribute_table;  // meta_element_table
+    private AttributeTable language_dependent_attribute_table; //lang_element_table; 
+
+    private MetadataSetTree meta_set_tree;     
+
+    private MetadataSetModel metadata_set_model;
+
+    private boolean stand_alone = true;
+
+    private ArrayList listeners;
+
+    static public void main(String[] args)
+    {
+	//TODO: add an option to open a paticular metadata set
+
+	// Parse arguments
+	GetOpt go = new GetOpt(args);
+
+	if (go.debug) {
+	    DebugStream.enableDebugging();
+
+	    Calendar now = Calendar.getInstance();
+	    String debug_file_path = "debug" + now.get(Calendar.DATE) + "-" + now.get(Calendar.MONTH) + "-" + now.get(Calendar.YEAR) + ".txt";
+	    
+	    // Debug file is created in the GLI directory
+	    DebugStream.println("Debug file path: " + debug_file_path);
+	    DebugStream.setDebugFile(debug_file_path);   
+
+	}
+
+	  new GEMS(go.gsdl_path,go.gsdl3_path,go.metadata_path,true,go.new_set);
+       
+    }
+
+
+    /** Constructor.
+     */
+    public GEMS(String gsdl_path, String gsdl3_path, String metadata_path, boolean standalone, boolean new_set)
+    {
+	self = this;
+	JarTools.initialise(this);        
+	screen_size = Configuration.screen_size;
+        msm = new MetadataSetManager(gsdl_path,gsdl3_path);       
+        stand_alone = standalone;
+	listeners = new ArrayList();
+
+        addWindowListener(new WindowAdapter(){
+		public void windowClosing(WindowEvent e){
+		    metadata_set_model.save(true);
+		    if (stand_alone){
+			System.exit(0);
+		    }
+		    else {
+			notifyListeners();
+			setVisible(false);
+		    }
+		}
+	    });
+	
+	setSize(SIZE);
+	setTitle(Dictionary.get("GEMS.Title"));
+	setJMenuBar(new GEMSMenuBar());
+
+	addWindowFocusListener(this);
+
+	card_layout = new CardLayout();
+	card_pane = new JPanel();
+
+        metadata_set_model = new MetadataSetModel(msm);
+        msm.setMetadataSetModel(metadata_set_model);
+	meta_set_tree = new MetadataSetTree(self);
+        metadata_set_model.addObserver(meta_set_tree);
+
+	attribute_table = new AttributeTable(false);
+	MetadataElementModel mem = new MetadataElementModel();
+        mem.addMetadataElementListener((MetadataElementListener)attribute_table);
+	attribute_table.addAttributeListener(metadata_set_model);
+
+	language_dependent_attribute_table = new AttributeTable(true);
+        mem.addMetadataElementListener((MetadataElementListener)language_dependent_attribute_table);
+	language_dependent_attribute_table.addAttributeListener(metadata_set_model);
+
+      	MetadataSetInfo msti= new MetadataSetInfo();
+        msti.addMetadataSetListener((MetadataSetListener)attribute_table);
+        msti.addMetadataSetListener((MetadataSetListener)language_dependent_attribute_table);
+        msti.setMetadataSetModel(metadata_set_model);
+        
+        open_prompt = new OpenMetadataSetPrompt(self,msm);
+        open_prompt.addMetadataSetListener((MetadataSetListener)metadata_set_model);
+        open_prompt.addMetadataSetListener((MetadataSetListener)attribute_table);        
+	open_prompt.addMetadataSetListener((MetadataSetListener)language_dependent_attribute_table); 	
+	
+        delete_prompt = new DeleteMetadataSetPrompt(self,msm);
+        delete_prompt.addMetadataSetListener((MetadataSetListener)metadata_set_model); 
+        delete_prompt.addMetadataSetListener((MetadataSetListener)attribute_table);
+	delete_prompt.addMetadataSetListener((MetadataSetListener)language_dependent_attribute_table);
+	
+        new_prompt = new NewMetadataSetPrompt(self,msm);
+        new_prompt.addMetadataSetListener((MetadataSetListener)metadata_set_model); 
+        new_prompt.addMetadataSetListener((MetadataSetListener)attribute_table);
+        new_prompt.addMetadataSetListener((MetadataSetListener)language_dependent_attribute_table);
+  
+
+        // load the initial metadataset
+        if (metadata_path !=null && !metadata_path.equals("")){
+	    open_prompt.openMetadataSet(metadata_path);
+	}
+	else{
+	    if (new_set) new_prompt.display();
+	}
+
+	// the set tree
+        JScrollPane treePane = new JScrollPane(meta_set_tree);
+	// the language independent attributes
+	JScrollPane tablePane = new JScrollPane(attribute_table);
+	// the language dependent attributes
+	JScrollPane langTablePane =  new JScrollPane(language_dependent_attribute_table);
+
+	// no set loaded pane
+	JPanel no_set_loaded_pane = new JPanel();
+	no_set_loaded_pane.setBackground(Color.lightGray);
+	JLabel no_set_loaded_label = new JLabel(Dictionary.get("GEMS.No_Set_Loaded"));
+	no_set_loaded_label.setHorizontalAlignment(JLabel.CENTER);
+	no_set_loaded_label.setVerticalAlignment(JLabel.CENTER);
+
+	no_set_loaded_pane.setLayout(new BorderLayout());
+	no_set_loaded_pane.add(no_set_loaded_label, BorderLayout.CENTER);
+
+        JPanel buttonPane = new JPanel(new GridLayout(1,2));
+        
+        JButton arrow_up_button = new GLIButton(Dictionary.get("GEMS.Move_Up"));
+	arrow_up_button.setIcon(JarTools.getImage("arrow-up.gif"));
+
+        JButton arrow_down_button = new GLIButton(Dictionary.get("GEMS.Move_Down"));
+	arrow_down_button.setIcon(JarTools.getImage("arrow-down.gif")); 
+        
+        arrow_up_button.setActionCommand(GEMSConstants.MOVE_UP);
+	arrow_up_button.addActionListener(meta_set_tree);
+        arrow_down_button.addActionListener(meta_set_tree);
+	arrow_down_button.setActionCommand(GEMSConstants.MOVE_DOWN);
+	
+	buttonPane.add(arrow_up_button);
+        buttonPane.add(arrow_down_button);
+        
+        JPanel leftPane =  new JPanel(new BorderLayout());          
+        leftPane.add(treePane,BorderLayout.CENTER);  
+	leftPane.add(buttonPane,BorderLayout.SOUTH);  
+         
+	JLabel selected_language_label = new JLabel(Dictionary.get("GEMS.SelectedLanguage"));
+	selected_language_label.setOpaque(true);
+        
+	Vector language_vector = new Vector(msm.getLanguageList());
+        language_vector.add(0, Dictionary.get("GEMS.Language"));
+	JComboBox language_combo = new JComboBox(language_vector);
+	
+	language_combo.addActionListener(new ActionListener(){
+		public void actionPerformed(ActionEvent e){
+		    JComboBox language_combo = (JComboBox) e.getSource();
+		    String langInfo = (String)language_combo.getSelectedItem(); 
+		    if (!langInfo.equals(Dictionary.get("GEMS.Language"))) {
+			String lang = langInfo.split("\\s")[0];     		    
+			language_dependent_attribute_table.addNewLanguage(lang);
+		    }
+		}
+
+	    });      
+
+       	JLabel language_label = new JLabel(Dictionary.get("GEMS.LanguageDependent"));
+	language_label.setOpaque(true);
+      
+         	
+	JPanel selectLangPane = new JPanel(new BorderLayout(5,5));
+        selectLangPane.add(selected_language_label,BorderLayout.WEST);
+        selectLangPane.add(language_combo, BorderLayout.CENTER);   
+
+
+        JPanel languagePane =  new JPanel(new BorderLayout(5,5));
+	languagePane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	languagePane.add(language_label,BorderLayout.NORTH);  
+        languagePane.add(selectLangPane,BorderLayout.CENTER);        
+
+
+        JPanel languageAttributePane =  new JPanel(new BorderLayout());
+	languageAttributePane.add(languagePane,BorderLayout.NORTH);  
+        languageAttributePane.add(langTablePane,BorderLayout.CENTER);        
+
+	JLabel attribute_label = new JLabel(Dictionary.get("GEMS.Attribute_Table"));
+	attribute_label.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	JPanel mainAttributePane = new JPanel(new BorderLayout());
+	mainAttributePane.add(attribute_label, BorderLayout.NORTH);
+	mainAttributePane.add(tablePane, BorderLayout.CENTER);
+	
+	attribute_tables_split_pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,mainAttributePane,languageAttributePane);
+	
+	metadata_set_details_split_pane =  new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,leftPane,attribute_tables_split_pane); 	
+
+	card_pane.setLayout(card_layout);
+	card_pane.add(no_set_loaded_pane, NO_SET_LOADED_CARD);
+	card_pane.add(metadata_set_details_split_pane, SET_LOADED_CARD);
+
+	getContentPane().add(card_pane,BorderLayout.CENTER);
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	if (stand_alone)
+	    setVisible(true);
+    }
+
+    public void setVisible(boolean visible) {
+	super.setVisible(visible);
+	if (visible) {
+	    attribute_tables_split_pane.setDividerLocation(0.3);
+	}
+    }
+    
+    // called from GLI
+    public void displayMetadataSet(String metadata_path){
+	open_prompt.openMetadataSet(metadata_path);
+	updateCardLayout(true);
+	setVisible(true);
+    }   
+
+    // called from GLI
+    public void newMetadataSet(){
+	new_prompt.display();
+	if (!new_prompt.isCancelled()) {
+	    updateCardLayout(true);
+	    setVisible(true);
+	}
+
+    }
+
+    public void addGEMSListener(GEMSListener listener){
+	listeners.add(listener);
+    }  
+
+    public void removeGEMSListener(GEMSListener listener){
+	listeners.remove(listener);
+    }  
+
+    public void notifyListeners(){
+	for (int i=0;i<listeners.size();i++){
+	    GEMSListener listener = (GEMSListener) listeners.get(i); 
+	    listener.gemsIsClosed();
+	}
+    }
+
+
+    public void exit(){
+	//clean up things
+	cleanUpListeners();
+    }
+    
+    protected void cleanUpListeners() {
+	MetadataSetInfo msti= new MetadataSetInfo();
+	msti.removeAllMetadataSetListeners();
+	MetadataElementModel mem= new MetadataElementModel();
+	mem.removeAllMetadataElementListeners();
+
+    }
+    
+    public void windowGainedFocus(WindowEvent e) {
+	//System.out.println("windowGainedFocus.");
+    }
+
+    public void windowLostFocus(WindowEvent e) {
+        //System.out.println("windowLostFocus.");
+          
+	//save stuff
+	
+    }
+
+    protected void updateCardLayout(boolean set_loaded) {
+	if (set_loaded) {
+	    card_layout.show(card_pane, SET_LOADED_CARD);
+	} else {
+	    card_layout.show(card_pane, NO_SET_LOADED_CARD);
+	}
+    }
+   
+    private class GEMSMenuBar
+	extends JMenuBar
+	implements ActionListener
+    {
+	private JMenu file = null;
+	private JMenu edit = null;
+
+	public JMenuItem file_exit   = null;
+	public JMenuItem file_new    = null;
+	public JMenuItem file_open   = null;
+	public JMenuItem file_close   = null;
+	public JMenuItem file_save   = null;
+	public JMenuItem edit_copy   = null;
+	public JMenuItem edit_cut    = null;
+	public JMenuItem edit_paste  = null;
+	public JMenuItem help_help   = null;
+        public JMenuItem file_delete = null;
+        //public JMenuItem file_preferences = null;
+	public GEMSMenuBar()
+	{
+	    file = new JMenu();
+	    file.setText(Dictionary.get("Menu.File"));
+	    
+	    file_exit = new JMenuItem(Dictionary.get("Menu.File_Exit"));
+	    file_exit.addActionListener(this);
+	    
+	    file_new = new JMenuItem(Dictionary.get("Menu.File_New"));
+	    file_new.addActionListener(this);
+
+	    file_open = new JMenuItem(Dictionary.get("Menu.File_Open"));
+	    file_open.addActionListener(this);
+	    
+	    file_close = new JMenuItem(Dictionary.get("Menu.File_Close"));
+	    file_close.addActionListener(this);
+
+	    file_save = new JMenuItem(Dictionary.get("Menu.File_Save"));
+	    file_save.addActionListener(this);
+   
+            file_delete = new JMenuItem(Dictionary.get("Menu.File_Delete"));
+	    file_delete.addActionListener(this);
+	                
+            //file_preferences = new JMenuItem(Dictionary.get("Menu.File_Options"));
+	    //file_preferences.addActionListener(this);
+	    
+	    if (!stand_alone) {
+		// when running from GLI, we disable most of the menu so 
+		// that we have control over what sets the user is editing
+		file_new.setEnabled(false);
+		file_open.setEnabled(false);
+		file_close.setEnabled(false);
+		file_delete.setEnabled(false);
+	    }
+	    
+	    // Layout (file menu)
+	    file.add(file_new);
+	    file.add(file_open);
+	    file.add(file_close);
+	    file.add(file_save);
+            file.add(file_delete);
+	    file.add(new JSeparator());
+            //file.add(file_preferences);
+            //file.add(new JSeparator());
+	    file.add(file_exit);
+            
+	    // Edit menu
+	    edit = new JMenu();
+	    edit.setText(Dictionary.get("Menu.Edit"));
+
+	    String modkey = "ctrl";
+	    if(Utility.isMac()) { // on Mac, we now use the Apple key instead of Ctrl for GLI/GEMS edit shortcuts
+		// http://stackoverflow.com/questions/5585919/creating-unicode-character-from-its-number
+		char appleKeyCodepoint = 0x2318; // applekey symbol unicode codepoint: U+2318 (\u2318)
+		String appleKeySymbol = String.valueOf(appleKeyCodepoint);
+		modkey = appleKeySymbol;
+	    }
+
+	    edit_cut = new JMenuItem(Dictionary.get("Menu.Edit_Cut", modkey));
+	    edit_cut.addActionListener(this);
+	    
+	    edit_copy = new JMenuItem(Dictionary.get("Menu.Edit_Copy", modkey));
+	    edit_copy.addActionListener(this);
+	    
+	    edit_paste = new JMenuItem(Dictionary.get("Menu.Edit_Paste", modkey));
+	    edit_paste.addActionListener(this);
+	    
+	    // Layout (edit menu)
+	    edit.add(edit_cut);
+	    edit.add(edit_copy);
+	    edit.add(edit_paste);
+
+	
+	    // Layout (menu bar)
+	    this.add(file);
+	    this.add(Box.createHorizontalStrut(15));
+	    this.add(edit);
+	    this.add(Box.createHorizontalGlue());
+	    //this.add(help);
+	}
+
+
+	public void actionPerformed(ActionEvent event)
+	{
+	    Object event_source = event.getSource();
+
+	    // File -> New
+	    if (event_source == file_new) {
+		new_prompt.display();
+		if (!new_prompt.isCancelled()) {
+		    updateCardLayout(true);
+		}
+ 		return;
+	    }
+
+	    // File -> Open
+	    if (event_source == file_open) {               
+		open_prompt.display();
+		if (!open_prompt.isCancelled()) {
+		    updateCardLayout(true);
+		}
+		return;
+	    }
+
+	    // File -> Close
+	    if (event_source == file_close) {
+		metadata_set_model.save(true); 
+		updateCardLayout(false);
+		return;
+	    }
+              // File -> Delete
+	    if (event_source == file_delete) {               
+                delete_prompt.display();
+		return;
+	    }
+
+
+	    // File -> Save
+	    if (event_source == file_save) {
+		metadata_set_model.save(false);
+		return;
+	    }        
+
+	    // File -> Exit
+	    if (event_source == file_exit) {
+               	metadata_set_model.save(true); 
+		if (stand_alone){
+		    System.exit(0);
+		}
+		else {
+		    self.notifyListeners();
+		    self.setVisible(false);
+		    return;
+		}
+	    }
+
+	    /// File -> Preferences
+	    // if(event_source == file_preferences){
+		// GEMSPreferences GemsPreferences = new GEMSPreferences();  
+            //}
+
+	    // Edit -> Cut
+	    if (event_source == edit_cut) {
+		try {
+		    KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
+		    // Get the component with selected text as a JTextComponent
+		    JTextComponent text = (JTextComponent) kfm.getPermanentFocusOwner();
+		    // Cut the text to the clipboard
+		    text.cut();
+		}
+		catch (ClassCastException cce) {
+		    // If the component is not a text component ignore the cut command
+		    DebugStream.println(cce.toString());
+		}
+		return;
+	    }
+
+	    // Edit -> Copy
+	    if (event_source == edit_copy) {
+		try {
+		    KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
+		    // Get the component with selected text as a JTextComponent
+		    JTextComponent text = (JTextComponent) kfm.getPermanentFocusOwner();//getFocusOwner();
+		    text.copy();
+		}
+		catch (Exception cce) {
+		    // If the component is not a text component ignore the copy command
+		    DebugStream.println(cce.toString());
+		}
+		return;
+	    }
+
+	    // Edit -> Paste
+	    if (event_source == edit_paste) {
+		try {
+		    KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
+		    // Get the component with selected text as a JTextComponent
+		    JTextComponent text = (JTextComponent) kfm.getPermanentFocusOwner();
+		    // Cut the text to the clipboard
+		    text.paste();
+		}
+		catch (ClassCastException cce) {
+		    // If the component is not a text component ignore the paste command
+		    DebugStream.println(cce.toString());
+		}
+		return;
+	    }
+	  
+	}
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/GEMSConstants.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/GEMSConstants.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/GEMSConstants.java	(revision 31635)
@@ -0,0 +1,94 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import org.greenstone.gatherer.Dictionary;
+
+public class GEMSConstants
+{
+    
+    static String NAMESPACE_ATTRIBUTE = "namespace";
+    static String NAME_ATTRIBUTE = "name";
+    static String DESCRIPTION_ATTRIBUTE = "description";
+    static String CODE_ATTRIBUTE = "code";
+    static String LANGUAGE_ATTRIBUTE = "language";
+    static String SET_LANGUAGE_ELEMENT = "SetLanguage";
+    static String LANGUAGE_ELEMENT = "Language";
+    static String NAME_ELEMENT = "Name";
+    static String DESCRIPTION_ELEMENT = "Description";
+    static String DEFAULT_LANGUAGE ="en";
+    static String ATTRIBUTE_ELEMENT="Attribute";
+    static String ELEMENT_ELEMENT="Element";  
+    static String METADATASET_ELEMENT="MetadataSet"; 
+    static String ADD_ATTRIBUTE = "addAttribute";
+    static String DELETE_ATTRIBUTE = "deleteAttribute";
+    static String ADD_ELEMENT = "addElement";
+    static String ADD_SUBELEMENT = "addSubElement";
+    static String DELETE_ELEMENT = "deleteElement";
+    static int METADATA_INFO = 0;
+    static int METADATA_ELEMENT = 0;
+    static String NEW_ELEMENT = "ELEMENT";
+    static String MOVE_UP = "moveUp";
+    static String MOVE_DOWN = "moveDown"; 
+    static String EXTRACTED_METADATA_NAMESPACE = "ex";
+
+    static String[] SET_REQUIRED_ATTRIBUTES = new String[]{"contact","creator","family","lastchanged","namespace"};
+    static String[] SET_LANG_DEPEND_ATTR_NAMES = new String[]{"name","description"};
+
+    static String UNKNOWN_NAME="unknown";  
+
+    static String MATADATASET_NAME="MetadataSet";
+    
+    static String SYSTEM_ID = "http://www.greenstone.org/dtd/MetadataSet/1.0/MetadataSet.dtd";
+
+    static String[] DIALOG_OPTIONS = new String[]{Dictionary.get("General.OK"),Dictionary.get("General.Cancel")};
+
+    static String LABEL_ATTRIBUTE = "label";
+    static String DEFINITION_ATTRIBUTE = "definition";
+    static String COMMENT_ATTRIBUTE = "comment";
+
+    static String [] ELEMENT_REQUIRED_ATTRIBUTES = new String[] {"name"};
+    
+    static String [] ELEMENT_LANG_DEPEND_ATTR_NAMES = new String[] {LABEL_ATTRIBUTE, DEFINITION_ATTRIBUTE, COMMENT_ATTRIBUTE};
+
+    static String [] ATTRIBUTES_COLUMN_NAMES = new String [] {
+	Dictionary.get("GEMS.AttributeTable.Name"), Dictionary.get("GEMS.AttributeTable.Value")};
+    static String [] LANG_DEPENDENT_ATTRIBUTES_COLUMN_NAMES = new String [] {
+	Dictionary.get("GEMS.AttributeTable.Name"), Dictionary.get("GEMS.AttributeTable.Language"), Dictionary.get("GEMS.AttributeTable.Value")};
+
+    static String METADATA_SET_FILE_EXTENSION = ".mds";
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/GEMSListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/GEMSListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/GEMSListener.java	(revision 31635)
@@ -0,0 +1,43 @@
+
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at th * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################## ##########################
+ */
+package org.greenstone.gatherer.gems;
+
+public interface GEMSListener{
+
+    public void gemsIsClosed();
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataElementEvent.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataElementEvent.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataElementEvent.java	(revision 31635)
@@ -0,0 +1,50 @@
+
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at th * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################## ##########################
+ */
+package org.greenstone.gatherer.gems;
+
+public class MetadataElementEvent{
+    private MetadataElementModel meta_element;
+
+    public MetadataElementEvent(MetadataElementModel mee){
+	meta_element = mee;
+
+    }
+
+    public MetadataElementModel getMetadataElementModel(){
+	return  meta_element;
+    } 
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataElementListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataElementListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataElementListener.java	(revision 31635)
@@ -0,0 +1,43 @@
+
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at th * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################## ##########################
+ */
+package org.greenstone.gatherer.gems;
+
+public interface MetadataElementListener{
+
+    public void metadataElementChanged(MetadataElementEvent mse);
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataElementModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataElementModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataElementModel.java	(revision 31635)
@@ -0,0 +1,350 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import org.greenstone.gatherer.util.XMLTools;
+import org.greenstone.gatherer.Configuration;
+
+import org.w3c.dom.Document;
+import  org.w3c.dom.Element;
+import  org.w3c.dom.Text;
+import  org.w3c.dom.Node;
+
+import java.util.Observable ;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * @author Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */
+public class MetadataElementModel
+    extends AttributeContainer 
+{
+    private Element metadata_element;
+    private ArrayList children;
+    private MetadataElementModel parent;
+    private static ArrayList listeners = new ArrayList(); 
+    private MetadataSetInfo metadataSetInfo;
+    private String name;
+    private static int count = 0;    
+    
+    public MetadataElementModel(){}
+    
+    // do we need this one?
+    public MetadataElementModel(MetadataSetInfo info){
+	this(info, null);
+    }
+    
+    public MetadataElementModel(MetadataSetInfo info, String new_name){
+	super(null, GEMSConstants.ELEMENT_LANG_DEPEND_ATTR_NAMES);
+	required_attribute_names = GEMSConstants.ELEMENT_REQUIRED_ATTRIBUTES;
+	metadataSetInfo = info;
+	children = new ArrayList(); 
+	if (new_name != null) {
+	    name = new_name;       
+	} else {
+	    name = GEMSConstants.NEW_ELEMENT+(count++);  
+	}
+	// add name in as an attribute - lang independent
+	Attribute attr = new Attribute(GEMSConstants.NAME_ATTRIBUTE, name, true);
+	attributes.add(attr);
+	// also add in a default label attribute
+	attr = getAttributeByNameAndLanguage(GEMSConstants.LABEL_ATTRIBUTE, Configuration.getLanguage());
+	attr.setValue(name);
+
+    }
+     
+    public MetadataElementModel(Element metadataElement, MetadataSetInfo info){
+	super(null, null);
+	required_attribute_names = GEMSConstants.ELEMENT_REQUIRED_ATTRIBUTES;
+	language_dependent_attribute_names = GEMSConstants.ELEMENT_LANG_DEPEND_ATTR_NAMES;
+	metadataSetInfo = info;
+	metadata_element = metadataElement;
+	
+	children = new ArrayList(); 
+	
+	constructModel(metadata_element);     
+	
+	ArrayList child_elements =XMLTools.getChildElementsByTagName(metadata_element,GEMSConstants.ELEMENT_ELEMENT);
+	
+	for(int i=0;i<child_elements.size();i++){    
+	    Element element = (Element)child_elements.get(i);
+	    MetadataElementModel submodel = new MetadataElementModel(element,metadataSetInfo); 
+	    submodel.setParent(this);    
+	    children.add(submodel);
+	}
+    }
+    
+    private void constructModel(Element meta_element){
+	name = meta_element.getAttribute(GEMSConstants.NAME_ATTRIBUTE);
+	
+	// add name in as an attribute - the only lang independent one
+	Attribute attr = new Attribute(GEMSConstants.NAME_ATTRIBUTE, name, true);
+	attributes.add(attr);
+
+	// common attributes
+	ArrayList attribute_nodes =XMLTools.getChildElementsByTagName(metadata_element,GEMSConstants.ATTRIBUTE_ELEMENT);
+	  
+	for(int i=0;i< attribute_nodes.size();i++){
+	    Element attr_element = (Element)attribute_nodes.get(i);
+	    String name = attr_element.getAttribute(GEMSConstants.NAME_ATTRIBUTE).trim();
+	     
+	    String value =  XMLTools.getElementTextValue(attr_element);
+	    attr = new Attribute(name,value);
+	    String lang = attr_element.getAttribute(GEMSConstants.LANGUAGE_ATTRIBUTE);
+	    if (lang.equals("")) {
+		// lang independent
+		attr.setRequired(isAttributeRequired(name));
+		attributes.add(attr);
+	    } else {
+		attr.setLanguage(lang);
+		attr.setRequired(isLangDependentAttributeRequired(name));
+		language_dependent_attributes.add(attr);
+	    }  
+	}
+	
+	//language specific attribute  
+	ArrayList language_elements =XMLTools.getChildElementsByTagName(metadata_element,GEMSConstants.LANGUAGE_ELEMENT);
+	  
+	for(int i=0;i< language_elements.size();i++){
+	    Element language_element = (Element)language_elements.get(i);
+	    String lang = language_element.getAttribute(GEMSConstants.CODE_ATTRIBUTE);
+	    //Attribute element
+	    ArrayList subattributes =XMLTools.getChildElementsByTagName(language_element,GEMSConstants.ATTRIBUTE_ELEMENT);
+	      
+	    for(int j=0;j< subattributes.size();j++){
+		Element attr_element = (Element)subattributes.get(j);
+		String name = attr_element.getAttribute(GEMSConstants.NAME_ATTRIBUTE).trim();
+		String value =  XMLTools.getElementTextValue(attr_element);
+		attr = new Attribute(name,value, isLangDependentAttributeRequired(name));
+		attr.setLanguage(lang);
+		language_dependent_attributes.add(attr);			    
+	    }
+	      
+	}
+	  
+    }
+      
+    public void addMetadataElementListener(MetadataElementListener msl){
+	if (!listeners.contains(msl))
+	    listeners.add(msl);   
+    }
+      
+
+    public void removeMetadataElementListener(MetadataElementListener msl){
+	listeners.remove(msl);   
+    } 
+      
+    public void removeAllMetadataElementListeners(){
+	listeners.clear();
+    }
+     
+      
+    public boolean doesChildWithThisNameExist(String name) {
+	for (int i=0; i<children.size(); i++) {
+	    if (name.equals(((MetadataElementModel)children.get(i)).getName())) {
+		return true;
+	    }
+	}
+	return false;
+    }
+
+
+    public Element getMetadataElement(){
+	return metadata_element;
+    }
+      
+    public void notifyListeners(boolean isDeleted){
+
+	MetadataElementEvent mee = new MetadataElementEvent(this);
+        if (!isDeleted){
+	    mee = new MetadataElementEvent(this);
+	}
+	else{
+	    mee = new MetadataElementEvent(null);
+	}
+	
+	for(int i=0;i<listeners.size();i++){
+	    MetadataElementListener msl = (MetadataElementListener) listeners.get(i);
+            msl.metadataElementChanged(mee);          
+	}
+    }
+
+    public ArrayList getChildElements(){
+	return children;
+    }
+
+    public void addChild(MetadataElementModel child){
+	metadataSetInfo.getMetadataSetModel().setChanged(true);
+	child.setParent(this);
+	children.add(child);
+    }
+
+    public void removeChild(MetadataElementModel element_model){
+	metadataSetInfo.getMetadataSetModel().setChanged(true);
+	children.remove(element_model);
+    }
+      
+    public String getName(){
+	return name;
+    }
+
+    public void setName(String new_name){
+	metadataSetInfo.getMetadataSetModel().setChanged(true);
+	name = new_name;
+              
+	MetadataSetModel metadata_model = metadataSetInfo.getMetadataSetModel();
+	metadata_model.valueChanged();
+    }
+
+    public String getFullName(){
+	if (parent == null){
+	    return metadataSetInfo.getNamespace() +"."+name;
+	}
+	else{
+	    return parent.getFullName() +"^"+ name;
+	}
+    }
+
+    public void setParent(MetadataElementModel pr){
+	parent = pr;
+    }
+
+    public MetadataElementModel getParent(){
+	return parent;
+    }
+
+        
+    public boolean moveUp(MetadataElementModel child){
+	metadataSetInfo.getMetadataSetModel().setChanged(true);
+	int index = children.indexOf(child);
+	if (index-1 >=0){
+	    children.remove(child);
+	    children.add(index-1,child);
+	    return true;
+	}        
+	return false;
+    }
+
+    public boolean moveDown(MetadataElementModel child){
+	metadataSetInfo.getMetadataSetModel().setChanged(true);
+	int index = children.indexOf(child);
+	if (index+1 < children.size()){
+	    children.remove(child);
+	    children.add(index+1,child);
+	    return true;
+	}        
+
+	return false;
+    }
+
+    public void save(){
+	saveElement();
+	 
+	for(int i=0;i<children.size();i++){
+	    MetadataElementModel model = (MetadataElementModel)children.get(i);
+	    model.save();
+	}   
+	    
+    }
+
+
+    private void saveElement(){
+	Document doc = metadataSetInfo.getMetadataSetDocument();        
+
+	Node parent_element = null;
+	  
+	if (parent !=null){
+	    parent_element = parent.getMetadataElement();
+	}
+	else{
+	    parent_element = doc.getDocumentElement();
+	}
+	  
+	  
+	metadata_element = save(doc);
+
+	parent_element.appendChild(metadata_element); 
+	                           
+	    
+    }
+
+
+    private Element save(Document doc){
+	
+	metadata_element = doc.createElement(GEMSConstants.ELEMENT_ELEMENT);
+	metadata_element.setAttribute(GEMSConstants.NAME_ATTRIBUTE,name);
+              
+	//non language dependent attributes
+	for(int i=0;i<attributes.size();i++){
+	    Attribute attr = (Attribute)attributes.get(i);
+	    if (attr.getName() == null || attr.getName().trim().equals("")) continue;
+	    if (attr.getName().equals("name")) continue;
+	    Element attrElement =  doc.createElement(GEMSConstants.ATTRIBUTE_ELEMENT);
+	    attrElement.setAttribute(GEMSConstants.NAME_ATTRIBUTE,attr.getName());
+	    attrElement.appendChild(doc.createTextNode(attr.getValue()));
+	    metadata_element.appendChild(attrElement);
+	}
+
+	  
+	//language dependent attributes
+	ArrayList langs = getAllLanguages();
+	for (int i=0;i<langs.size();i++){
+	    String lang = (String)langs.get(i);
+	    Element langElement =  doc.createElement(GEMSConstants.LANGUAGE_ELEMENT);
+	    langElement.setAttribute(GEMSConstants.CODE_ATTRIBUTE,lang);
+	    ArrayList langAttrs = getAttributesByLanguage(lang);
+	    for (int j=0;j<langAttrs.size();j++){
+		Attribute attr = (Attribute)langAttrs.get(j);
+		Element attrElement =  doc.createElement(GEMSConstants.ATTRIBUTE_ELEMENT);
+		attrElement.setAttribute(GEMSConstants.NAME_ATTRIBUTE,attr.getName());
+		attrElement.appendChild(doc.createTextNode(attr.getValue()));  
+		langElement.appendChild(attrElement);
+	                    
+	    }
+	    metadata_element.appendChild(langElement);   
+	}              
+
+	return metadata_element;
+           
+    }
+
+ 
+  
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetEvent.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetEvent.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetEvent.java	(revision 31635)
@@ -0,0 +1,50 @@
+
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at th * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################## ##########################
+ */
+package org.greenstone.gatherer.gems;
+
+public class MetadataSetEvent{
+    private MetadataSetInfo meta_info;
+
+    public MetadataSetEvent(MetadataSetInfo info){
+	meta_info = info;
+
+    }
+
+    public MetadataSetInfo getMetadataSetInfo(){
+	return  meta_info;
+    } 
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetInfo.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetInfo.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetInfo.java	(revision 31635)
@@ -0,0 +1,333 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import org.apache.xerces.dom.*;// for new Documents
+import org.w3c.dom.Document; 
+import org.w3c.dom.DocumentType; 
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.Attr;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.NodeList;
+
+import java.util.HashMap;
+import java.util.ArrayList;
+
+import org.greenstone.gatherer.util.XMLTools;
+
+/**
+ * @author Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ */
+public class MetadataSetInfo
+    extends AttributeContainer
+{
+ 	
+    private String file_path="";  
+
+    private static ArrayList listeners = new ArrayList();  
+
+    private static MetadataSetModel metadata_model;
+       
+    private String current_language = GEMSConstants.DEFAULT_LANGUAGE;
+
+    private boolean isNew = false;
+
+    public MetadataSetInfo(){       
+	super(GEMSConstants.SET_REQUIRED_ATTRIBUTES, GEMSConstants.SET_LANG_DEPEND_ATTR_NAMES);
+    } 
+
+    public boolean isNamespaceAlreadyUsed(String namespace){
+	if(metadata_model != null) {
+	    return metadata_model.getMetadataSetManager().isNamespaceAlreadyUsed(namespace);
+	}
+	return false;
+    }
+
+    public void setNew(boolean isNew){
+	this.isNew = isNew;
+    }
+
+    public boolean isNew(){
+	return isNew;
+    }
+    
+    public Document getMetadataSetDocument(){
+	return  metadata_model.getMetadataSetDocument();
+    }
+    
+    public MetadataSetModel getMetadataSetModel(){
+        return metadata_model;
+    } 
+    
+     public void setMetadataSetModel(MetadataSetModel model){
+	 metadata_model = model;
+     } 
+    
+
+    public String getMetadataSetName(){  
+	return getMetadataSetName(getCurrentLanguage());
+    }
+
+    public String getMetadataSetName(String lang){    
+        Attribute attr = getAttributeByNameAndLanguage(GEMSConstants.NAME_ATTRIBUTE,lang);
+	if (attr != null) { 
+	    return attr.getValue();
+	}
+	else {
+	    return GEMSConstants.UNKNOWN_NAME;
+	}      
+    }
+
+     public void setMetadataSetName(String name){ 
+	 setMetadataSetName(name, getCurrentLanguage());
+     }
+
+    public void setMetadataSetName(String name,String lang){  
+	Attribute attr =  getAttributeByNameAndLanguage(GEMSConstants.NAME_ATTRIBUTE,lang);
+         if (attr != null){ 
+	     attr.setValue(name);
+	 } 
+	 else {
+	     attr = new Attribute(GEMSConstants.NAME_ATTRIBUTE, name, lang, true);
+	     language_dependent_attributes.add(attr);    
+	 }
+    }
+
+
+  
+    public String getMetadataSetDescription(){  
+	return getMetadataSetDescription(getCurrentLanguage());	 
+    }
+    
+    public String getMetadataSetDescription(String lang){    
+	Attribute attr = getAttributeByNameAndLanguage(GEMSConstants.DESCRIPTION_ATTRIBUTE,lang); 
+	if (attr != null) { 
+	    return attr.getValue();
+	}
+	else {
+	    return "";
+	}	 
+    }
+ 
+    public void setMetadataSetDescription(String desc) {
+	setMetadataSetDescription(desc, getCurrentLanguage());
+    }
+    
+    public void setMetadataSetDescription(String desc, String lang) {
+	Attribute attr =  getAttributeByNameAndLanguage(GEMSConstants.DESCRIPTION_ATTRIBUTE,lang);
+         if (attr != null){ 
+	     attr.setValue(desc);
+	 }
+	 else {
+	     attr = new Attribute(GEMSConstants.DESCRIPTION_ATTRIBUTE, desc, lang, true);
+	     language_dependent_attributes.add(attr);    
+	 }  
+    }
+    
+    public String getCurrentLanguage(){
+	return current_language;	
+    }
+
+    public void setCurrentLanguage(String lang){
+	current_language = lang;	
+    }
+    
+    public String getNamespace(){
+      	Attribute attr = getAttributeByName(GEMSConstants.NAMESPACE_ATTRIBUTE); 
+	if (attr != null){ 
+	    return attr.getValue();
+	 }
+	else{
+	    return "";
+	 }       
+    }
+  
+     public void setNamespace(String namespace){  
+	Attribute attr =  getAttributeByName(GEMSConstants.NAMESPACE_ATTRIBUTE);
+         if (attr != null){ 
+	     attr.setValue(namespace);
+	 }
+	 else {
+	     attr = new Attribute(GEMSConstants.NAMESPACE_ATTRIBUTE, namespace, true);
+	     attributes.add(attr);
+	 }    
+
+    }
+
+  
+    public String getFilePath(){
+	return file_path;
+    }     
+    
+    
+    public void infoChanged(){
+	metadata_model.valueChanged();
+    }
+    
+    public void setFilePath(String path){
+        file_path = path;
+    }     
+    
+    
+    public void addMetadataSetListener(MetadataSetListener msl){
+	if (!listeners.contains(msl))
+	    listeners.add(msl);   
+    }
+    
+    public void removeMetadataSetListener(MetadataSetListener msl){
+        listeners.remove(msl);   
+    }
+     
+    public void removeAllMetadataSetListeners(){
+	  listeners.clear();
+    }
+
+    public void deleteMetadataSet(){
+	metadata_model.getMetadataSetManager().deleteMetadataSet(this);
+
+      	if (metadata_model.getMetadataSetInfo() != null 
+	    && metadata_model.getMetadataSetInfo().getFilePath().equals(this.getFilePath())){
+	    metadata_model.metadataSetDeleted();
+	    MetadataSetEvent mse = new MetadataSetEvent(null);
+	    for(int i=0;i<listeners.size();i++){
+		MetadataSetListener msl = (MetadataSetListener) listeners.get(i);
+		msl.metadataSetChanged(mse);          
+	    }
+	}
+    }
+
+    public void notifyListeners(){
+        MetadataSetEvent mse = new MetadataSetEvent(this);
+	for(int i=0;i<listeners.size();i++){
+	    MetadataSetListener msl = (MetadataSetListener) listeners.get(i);
+	    msl.metadataSetChanged(mse);          
+	}
+    }
+ 
+    public void save(){
+	Document doc = metadata_model.getMetadataSetDocument();
+
+        //new metadata set
+        if (doc == null){
+	    doc = new DocumentImpl(); 
+            DocumentType docType = new DocumentTypeImpl((DocumentImpl)doc,GEMSConstants.MATADATASET_NAME,null,GEMSConstants.SYSTEM_ID);                         
+	    doc.appendChild(doc.createElement(GEMSConstants.METADATASET_ELEMENT));
+	    doc.appendChild(docType);
+	    metadata_model.setMetadataSetDocument(doc);          
+	} 
+    
+        Element doc_element = doc.getDocumentElement(); 
+	//remove all attributes
+	NamedNodeMap attrs = doc.getDocumentElement().getAttributes();
+	for (int i=0;i<attrs.getLength();i++){
+	    Attr item = (Attr)attrs.item(i);
+	    doc_element.removeAttribute(item.getName());
+	}
+       
+	//add attributes
+	for (int i=0;i<attributes.size();i++){
+	    Attribute attr = (Attribute)attributes.get(i);
+	    if (attr.getName() == null || attr.getName().trim().equals("")) continue;
+            doc_element.setAttribute(attr.getName(),attr.getValue());
+	}
+
+        //remove all language dependent name and description elements
+	NodeList setLanguages = doc_element.getElementsByTagName(GEMSConstants.SET_LANGUAGE_ELEMENT);  
+
+	for (int i=setLanguages.getLength()-1;i>=0;--i){
+	    Node set_language = setLanguages.item(i);
+	    doc_element.removeChild(set_language);
+	}
+
+	 ArrayList names = XMLTools.getChildElementsByTagName(doc_element,GEMSConstants.NAME_ELEMENT);   
+         for(int i=0;i< names.size();i++){
+	     Element name_element = (Element)names.get(i);
+	     doc_element.removeChild(name_element);
+	 } 
+
+        ArrayList deses = XMLTools.getChildElementsByTagName(doc_element,GEMSConstants.DESCRIPTION_ELEMENT);   
+         for(int i=0;i< deses.size();i++){
+	     Element des_element = (Element)deses.get(i);
+	     doc_element.removeChild(des_element);
+	 } 
+
+	 Node first_child = doc_element.getFirstChild(); 
+
+	 Node previous_element = null;
+
+	 //add setLanguage elements
+        ArrayList langs = getAllLanguages();
+	for (int i=0;i<langs.size();i++){
+	    String lang = (String)langs.get(i);
+	    Attribute name = getAttributeByNameAndLanguage(GEMSConstants.NAME_ATTRIBUTE,lang);
+	    if (name !=null){
+		Element set_element = doc.createElement(GEMSConstants.SET_LANGUAGE_ELEMENT); 
+		set_element.setAttribute(GEMSConstants.CODE_ATTRIBUTE,lang);
+		Element name_element = doc.createElement(GEMSConstants.NAME_ELEMENT);
+                name_element.appendChild(doc.createTextNode(name.getValue()));
+		set_element.appendChild(name_element); 
+               	Attribute des = getAttributeByNameAndLanguage(GEMSConstants.DESCRIPTION_ATTRIBUTE,lang);
+                 if (des !=null){
+		     Element des_element = doc.createElement(GEMSConstants.DESCRIPTION_ELEMENT);
+		     des_element.appendChild(doc.createTextNode(des.getValue()));
+		     set_element.appendChild(des_element);
+		 }	 
+
+		 if (first_child == null){
+		    
+		     if (previous_element !=null && previous_element.getNextSibling() !=null){ 
+                         doc_element.insertBefore(set_element,previous_element.getNextSibling());  
+		     }
+		      else{
+			  doc_element.appendChild(set_element);
+		      }
+		 }
+		 else {
+		     doc_element.insertBefore(set_element,first_child);
+		     first_child = null;
+		     previous_element = set_element;
+		 }
+	    }
+	}
+
+           
+    }
+
+ 
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetListener.java	(revision 31635)
@@ -0,0 +1,43 @@
+
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at th * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################## ##########################
+ */
+package org.greenstone.gatherer.gems;
+
+public interface MetadataSetListener{
+
+    public void metadataSetChanged(MetadataSetEvent mse);
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetManager.java	(revision 31635)
@@ -0,0 +1,385 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import java.io.*;
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import java.util.ArrayList;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.file.*;
+import org.greenstone.gatherer.util.XMLTools;
+import org.greenstone.gatherer.Dictionary;
+
+import org.apache.xerces.parsers.*;
+import org.apache.xml.serialize.*;
+import org.w3c.dom.*;
+import org.xml.sax.*;
+
+
+
+/** This class is responsible for managing all the metadata sets in the collection and for providing methods for manipulating the aforementioned sets contents. 
+ * @author Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */
+public class MetadataSetManager
+{
+   
+    private ArrayList mds_list = new ArrayList();                      
+    
+    private MetadataSetModel metadata_set_model;
+
+    private ArrayList languageList; 
+
+    private String current_language = GEMSConstants.DEFAULT_LANGUAGE; 
+
+    /** Constructor. */
+    public MetadataSetManager(String gsdl_path, String gsdl3_path) {
+    	// Determine the GLI user directory path
+	String gli_user_directory_path = System.getProperty("user.home") + File.separator;
+	
+        if (Utility.isWindows()) {
+	    gli_user_directory_path += "Application Data" + File.separator + "Greenstone" + File.separator + "GLI" + File.separator;
+        }
+        else {
+            gli_user_directory_path += ".gli" + File.separator;
+        }
+
+	new Configuration(gli_user_directory_path, gsdl_path, gsdl3_path, null, null, null);         
+	current_language = Configuration.getLanguage(); 
+
+        languageList = new ArrayList();
+    }
+
+    public ArrayList getLanguageList(){
+        //already loaded
+	if (languageList.size() > 0 ) return languageList;
+  	
+	Document document = XMLTools.parseXMLFile(getLanguageXMLPath(), true);        
+
+	NodeList languages = document.getDocumentElement().getElementsByTagName(GEMSConstants.LANGUAGE_ELEMENT);
+        for(int i=0; i< languages.getLength();i++){
+            Element language_element = (Element)languages.item(i);
+            languageList.add(language_element.getAttribute(GEMSConstants.CODE_ATTRIBUTE).toLowerCase()+"   "+language_element.getAttribute(GEMSConstants.NAME_ATTRIBUTE));
+	}  
+   
+	return languageList;
+    }
+
+
+    public boolean isAttributeRequired(String name){
+	String tmp_name = name.trim();
+	for(int i=0;i<GEMSConstants.SET_REQUIRED_ATTRIBUTES.length;i++){
+            if (tmp_name.equals(GEMSConstants.SET_REQUIRED_ATTRIBUTES[i])) return true;
+	}
+	return false;
+    }
+
+    public void save(boolean confirm) {
+	MetadataSetInfo meta_info =  metadata_set_model.getMetadataSetInfo();
+	
+	if (meta_info ==null) return;
+
+	if (confirm){
+
+	    //int result = JOptionPane.showOptionDialog(null, Dictionary.get("GEMS.Confirm_Save"), Dictionary.get("GEMS.Confirm_Save_Title"), JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE,null,Dictionary.get("General.No"),Dictionary.get("General.No"));
+	    int result = JOptionPane.showConfirmDialog(null, Dictionary.get("GEMS.Confirm_Save"), Dictionary.get("GEMS.Confirm_Save_Title"), JOptionPane.YES_NO_OPTION);
+	    if (result != JOptionPane.YES_OPTION) return;
+	}
+	
+
+        File file_path = null;
+	//get new file name
+        if(meta_info.isNew()){
+	    JFileChooser file_chooser =  new JFileChooser(new File(getGLIMetadataDirectoryPath()));
+	    file_chooser.setMultiSelectionEnabled(false);
+	    file_chooser.setSelectedFile(new File(getGLIMetadataDirectoryPath(), meta_info.getNamespace()+GEMSConstants.METADATA_SET_FILE_EXTENSION));
+	    int result = file_chooser.showSaveDialog(null);
+	    if (result == JFileChooser.APPROVE_OPTION){
+		file_path = file_chooser.getSelectedFile();
+		meta_info.setFilePath(file_path.toString());
+		meta_info.setNew(false);
+	    } 
+	}
+	else {
+	    file_path = new File(meta_info.getFilePath());
+	}
+
+	if (file_path != null) save(file_path);
+
+	for(int i=0;i<mds_list.size();i++){
+	    MetadataSetInfo infoItem = (MetadataSetInfo)mds_list.get(i);
+	    if (infoItem.getFilePath().trim().equals(meta_info.getFilePath().trim())) {
+		mds_list.remove(infoItem);
+		mds_list.add(meta_info); 
+		break;
+	    }	    
+	}
+	return;
+
+    }
+
+
+    private void save(File file_path){
+
+	try {
+	    
+	    XMLTools.writeXMLFile(file_path, metadata_set_model.getMetadataSetDocument());
+	    
+	}
+	catch (Exception error) {
+	    error.printStackTrace();
+	}
+    
+    }
+
+
+    public Document getMetadataSetDocument(String filePath){ 
+	if (filePath == null || filePath.equals("")) return null;
+        return  XMLTools.parseXMLFile(filePath, true);
+    }
+
+    public void setMetadataSetModel(MetadataSetModel model){
+	metadata_set_model = model;
+    }
+
+    public MetadataSetModel getMetadataSetModel(){
+        return  metadata_set_model;
+    }
+    
+
+    public ArrayList getAvailableMetadataSets(){
+	mds_list.clear();        
+	// Load all the core metadata sets (in the GLI "metadata" directory)
+	File metadata_directory = new File(getGLIMetadataDirectoryPath());
+	if (metadata_directory.exists()) {
+	    // Load just those .mds files in this directory, and return them
+	    File[] directory_files = metadata_directory.listFiles();
+	    for (int i = 0; i < directory_files.length; i++) {
+		File child_file = directory_files[i];
+               
+     		if (!child_file.isDirectory() && child_file.getName().endsWith(GEMSConstants.METADATA_SET_FILE_EXTENSION)) {
+                   Document document = XMLTools.parseXMLFile(child_file.toString(), true);
+                   MetadataSetInfo info = constructMetadataInfo(document);
+                   info.setFilePath(child_file.toString());                              
+		   mds_list.add(info);		        	    
+		}
+	    }
+	}
+
+	return  mds_list;
+    }
+
+    public boolean isNamespaceAlreadyUsed(String namespace){
+	for(int i=0;i<mds_list.size();i++){
+	    MetadataSetInfo info = (MetadataSetInfo)mds_list.get(i);
+	    if (info.getNamespace().trim().equals(namespace)) return true;	    
+	}
+	return false;
+    }
+
+    public MetadataSetInfo getMetadataSet(String metadata_path){
+	MetadataSetInfo info = constructMetadataInfo(XMLTools.parseXMLFile(metadata_path,true));
+
+	// check whether there is metadata set name for the current language
+	// if not reset the current_language to the language first encountered
+        ArrayList attrs = info.getLanguageDependentAttributes();
+	boolean foundLanguage = false;
+        for (int i=0;i<attrs.size();i++){
+	    Attribute attr = (Attribute)attrs.get(i);
+	    if (attr.getLanguage().equals(current_language)) {
+		info.setCurrentLanguage(current_language);
+		foundLanguage = true;
+                break;
+	    }
+	    
+	}              
+        if (!foundLanguage && attrs.size()>0){
+	    info.setCurrentLanguage(((Attribute)attrs.get(0)).getLanguage());
+	}      
+	
+	info.setFilePath(metadata_path);            
+        return info;
+
+    }     
+    
+    public void deleteMetadataSet(MetadataSetInfo info){
+	String filepath = info.getFilePath();
+
+        if(filepath != null && !filepath.trim().equals("")){
+	    File file = new File(filepath);
+	  
+	  boolean deleted = file.delete();
+	  if (!deleted){
+	        JOptionPane.showMessageDialog(null,Dictionary.get("GEMS.File_Deletion_Error_Message"), Dictionary.get("GEMS.File_Deletion_Error"),JOptionPane.ERROR_MESSAGE);
+	  }
+	  else{
+	      	for(int i=0;i<mds_list.size();i++){
+		    MetadataSetInfo infoItem = (MetadataSetInfo)mds_list.get(i);
+		    if (infoItem.getFilePath().trim().equals(info.getFilePath().trim())) {
+			mds_list.remove(infoItem);
+			break;
+		    }	    
+		}
+	  }
+	}
+    }
+
+    public String getCurrentLanguage(){
+	return current_language;
+    }
+
+    private MetadataSetInfo constructMetadataInfo(Document document){      
+	NamedNodeMap attrs = document.getDocumentElement().getAttributes();
+        ArrayList attrList = new ArrayList();
+        String lang = null;
+        boolean findLang = false;
+	
+        for (int i=0;i<attrs.getLength();i++){
+	    Attr item = (Attr)attrs.item(i);
+	    Attribute attr = new Attribute(item.getName(),item.getValue());
+            attr.setRequired(isAttributeRequired(attr.getName()));
+	    attrList.add(attr);
+	}
+
+	ArrayList languageList = new ArrayList();
+ 
+        //load all <SetLanguage> elements
+        NodeList setLanguages = document.getDocumentElement().getElementsByTagName(GEMSConstants.SET_LANGUAGE_ELEMENT);
+	
+        if (setLanguages.getLength() >0){
+	    for(int i=0;i<setLanguages.getLength();i++){
+		Element set_language = (Element)setLanguages.item(i);
+                lang = set_language.getAttribute(GEMSConstants.CODE_ATTRIBUTE); 
+		setNameAndDescription(languageList, set_language,lang); 	
+	    }
+	    
+	}
+        //no set langugae elements, try load name and description elements
+        else{
+            setNameAndDescription(languageList, document.getDocumentElement(),lang);     
+	}
+	
+	MetadataSetInfo info = new MetadataSetInfo();
+	info.setLanguageDependentAttributes(languageList);
+	info.setAttributes(attrList);       
+	return info;
+    }
+
+    private void setNameAndDescription(ArrayList languageList, Element parent,String lang){
+
+	ArrayList names = XMLTools.getChildElementsByTagName(parent,GEMSConstants.NAME_ELEMENT);         
+        String name_lang = "";       
+
+	if (names.size() > 0){
+	    Element name_element = (Element)names.get(0);
+            Attribute attr = new Attribute(GEMSConstants.NAME_ATTRIBUTE,XMLTools.getElementTextValue(name_element), true);
+	    if (lang == null){
+		attr.setLanguage(name_element.getAttribute(GEMSConstants.LANGUAGE_ATTRIBUTE));
+                name_lang = attr.getLanguage();
+	    }
+	    else{
+		attr.setLanguage(lang); 
+	    }
+	    languageList.add(attr);
+// 	    if (current_language.equals(attr.getLanguage())){
+// 		attr.setRequired(true);
+//                 languageList.add(0,attr);
+// 	    }
+// 	    else{
+// 		languageList.add(attr);
+// 	    }    
+            
+	}
+      	
+	
+	ArrayList des = XMLTools.getChildElementsByTagName(parent,GEMSConstants.DESCRIPTION_ELEMENT);
+	if (des.size() > 0){
+	    Element des_element = (Element)des.get(0);
+	    Attribute attr = new Attribute(GEMSConstants.DESCRIPTION_ATTRIBUTE,XMLTools.getElementTextValue(des_element), true); 
+	    if (lang == null){
+		attr.setLanguage(des_element.getAttribute(GEMSConstants.LANGUAGE_ATTRIBUTE));
+	    }
+	      else{
+		  attr.setLanguage(lang); 
+	      }
+	    languageList.add(attr);
+	}
+	else{
+	    Attribute attr = new Attribute(GEMSConstants.DESCRIPTION_ATTRIBUTE,"", true);
+            if (lang == null){
+		attr.setLanguage(name_lang);
+	    }
+	    else{
+		attr.setLanguage(lang); 
+	    }
+	    
+	    languageList.add(attr); 
+	}
+	
+    }
+
+    
+    static public String getGLIDirectoryPath()
+    {
+	return System.getProperty("user.dir") + File.separator;
+    }
+
+
+    static public String getGLIMetadataDirectoryPath()
+    {
+	return getGLIDirectoryPath() + "metadata" + File.separator;
+    }
+  
+    static public String getLanguageXMLPath()
+    {
+	return getGLIDirectoryPath() + "classes" + File.separator+"xml"+ File.separator+ "languages.xml";
+    }
+      
+     static public String getCollectionPath()
+    {
+        if (Configuration.gsdl3_path != null){
+	    return Configuration.gsdl3_path + File.separator + "sites"; 
+
+	}
+	else{
+	    return Configuration.gsdl_path + File.separator + "collect";
+	}
+    } 
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetModel.java	(revision 31635)
@@ -0,0 +1,216 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import org.w3c.dom.Document;
+import  org.w3c.dom.Element;
+import org.greenstone.gatherer.util.XMLTools;
+
+ 
+import java.util.Observable;
+import java.util.ArrayList;
+
+/**
+ * @author Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */
+public class MetadataSetModel 
+    extends Observable implements MetadataSetListener, AttributeListener{
+
+    private Document metadata_doc;
+
+    private MetadataSetManager meta_manager;
+
+    private ArrayList model;      
+
+    private MetadataSetInfo meta_info;
+    
+    private boolean changed = false;
+
+
+    //construt a new MetadataSet
+    public MetadataSetModel(MetadataSetManager msm){
+        meta_manager = msm;
+	model = new ArrayList(); 
+    }
+
+    public String getCurrentLanguage(){
+	return meta_manager.getCurrentLanguage();
+    }
+
+    public void metadataSetChanged(MetadataSetEvent mse){
+	if (meta_info !=null) save(true); //save the last metadata set
+
+	model = new ArrayList();
+        meta_info = mse.getMetadataSetInfo();
+	meta_info.setMetadataSetModel(this);
+	metadata_doc = meta_manager.getMetadataSetDocument(meta_info.getFilePath());     
+	
+	constructModel();
+    
+        setChanged();
+	notifyObservers();
+	clearChanged();
+     	changed = false;  
+   
+    } 
+
+    public void valueChanged(){
+        setChanged();
+	notifyObservers(new Boolean(false));
+	clearChanged();
+    }
+    
+
+    private void constructModel(){
+	if (metadata_doc == null) return;
+	ArrayList elements =XMLTools.getChildElementsByTagName(metadata_doc.getDocumentElement(),GEMSConstants.ELEMENT_ELEMENT);
+	for(int i=0;i<elements.size();i++){    
+	    Element element = (Element)elements.get(i);     
+	    model.add(new MetadataElementModel(element,meta_info));
+	}                       
+    }
+    
+    public boolean doesChildWithThisNameExist(String name) {
+	for (int i=0; i<model.size(); i++) {
+	    if (name.equals(((MetadataElementModel)model.get(i)).getName())) {
+		return true;
+	    }
+	}
+	return false;
+    }
+
+    public Document getMetadataSetDocument(){
+	return metadata_doc;
+    }
+
+    public void setMetadataSetDocument(Document doc){
+	metadata_doc = doc;
+    }
+
+    public MetadataSetManager getMetadataSetManager(){
+	return meta_manager;
+    } 
+ 
+    public ArrayList getMetadataSetModel(){
+	return model;
+    }
+
+    
+    public void addChild(MetadataElementModel element_model){
+	model.add(element_model);
+	changed = true;
+    }
+    
+    public void removeChild(MetadataElementModel element_model){
+	model.remove(element_model);
+	changed = true;
+    }
+
+   
+    public void attributeChanged(AttributeEvent ae){      
+    	changed = true;
+    }
+    
+    public void setChanged(boolean ischanged){
+	changed = ischanged;
+    }
+
+    public void save(boolean confirm){
+	if ( meta_info == null || (!changed  && !meta_info.isNew())) return;
+
+	meta_info.save();
+	    
+	if (metadata_doc !=null){
+	    //remove all element elements
+	    Element doc_element = metadata_doc.getDocumentElement();
+	    ArrayList elements = XMLTools.getChildElementsByTagName(doc_element,GEMSConstants.ELEMENT_ELEMENT);   
+	    for(int i=0;i< elements.size();i++){
+		Element element = (Element)elements.get(i);
+		doc_element.removeChild(element);
+	    } 
+	}
+
+	for(int i=0;i< model.size();i++){
+	    MetadataElementModel element_model = (MetadataElementModel)model.get(i);
+            element_model.save();
+	} 
+	
+	meta_manager.save(confirm);
+	changed = false;
+	
+    }
+
+    public MetadataSetInfo getMetadataSetInfo(){
+	return meta_info; 
+    } 
+
+    public void metadataSetDeleted(){
+	meta_info = null;
+	setChanged();
+	notifyObservers();
+	clearChanged();
+     	changed = false; 	
+
+    }
+
+
+    public boolean moveUp(MetadataElementModel child){
+	int index = model.indexOf(child);
+	if (index-1 >=0){
+	    changed = true;
+	    model.remove(child);
+	    model.add(index-1,child);
+	    return true;
+	}        
+
+	return false;
+    }
+
+    public boolean moveDown(MetadataElementModel child){
+	int index = model.indexOf(child);
+	if (index+1 < model.size()){
+	    changed = true;
+	    model.remove(child);
+	    model.add(index+1,child);
+	    return true;
+	}        
+
+	return false;
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetTree.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetTree.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/MetadataSetTree.java	(revision 31635)
@@ -0,0 +1,435 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import org.w3c.dom.Document;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.tree.*; 
+
+import java.util.Observer;
+import java.util.Observable;
+import java.util.ArrayList;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+
+/**
+ * @author Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */
+public class MetadataSetTree 
+    extends JTree 
+    implements Observer, ActionListener, MouseListener {
+    
+    private MetadataSetTree self;
+    private MetadataSetModel metadata_model;
+    protected JFrame parent_frame;
+    public MetadataSetTree(JFrame parent){
+        this.setComponentOrientation(Dictionary.getOrientation());
+        setCellRenderer(new MetadataSetCellRenderer());
+        setRowHeight(20);
+        setEditable(false);
+        getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+	setModel(null);
+        self = this;
+	parent_frame = parent;
+	addMouseListener(this);	
+    }
+    
+    public void mouseClicked(MouseEvent event) {
+	// right click
+	if (SwingUtilities.isRightMouseButton(event)) {
+            new MetadataSetTreeRightClickMenu(this, event);
+        } else {
+	    TreePath tse = self.getSelectionPath();
+	    if (tse!=null){
+		DefaultMutableTreeNode snode= (DefaultMutableTreeNode)tse.getLastPathComponent();
+		
+		Object data = snode.getUserObject();
+		
+		if (data instanceof MetadataElementModel){
+		    
+		    ((MetadataElementModel)data).notifyListeners(false);
+		}
+		else{
+		    if (data instanceof MetadataSetInfo){
+			((MetadataSetInfo)data).notifyListeners();
+		    }
+		}			  
+	    }
+	}
+	
+    }
+    
+    public void mouseEntered(MouseEvent event) { }
+
+    public void mouseExited(MouseEvent event) { }
+
+    public void mousePressed(MouseEvent event) { }
+
+    public void mouseReleased(MouseEvent event) { }
+
+    public void update(Observable o,Object arg){ 
+        if (arg !=null){
+	    Boolean replaceModel = (Boolean)arg;
+            if (!replaceModel.booleanValue()){ 
+                repaint();               
+	 	expandAll();
+	    }
+	}      
+        metadata_model = (MetadataSetModel)o;
+
+	populateMetadataSetTree(metadata_model);
+
+    }
+
+    private void expandAll() { 
+	int row = 0; 
+	while (row < getRowCount()) {
+	    expandRow(row);
+	    row++;
+	}
+    } 
+
+
+
+    public void actionPerformed(ActionEvent e){
+        String command = e.getActionCommand();
+   
+        if (command.equals(GEMSConstants.ADD_ELEMENT)){
+	    addElement(false);
+ 	    return ;
+	}
+        if (command.equals(GEMSConstants.ADD_SUBELEMENT)){
+	    addElement(true);
+ 	    return ;
+	}
+       
+	if (command.equals(GEMSConstants.DELETE_ELEMENT)){
+	    deleteElement();
+	}          
+
+
+	if (command.equals(GEMSConstants.MOVE_UP)){
+	    moveUpElement();
+	}
+       
+	if (command.equals(GEMSConstants.MOVE_DOWN)){
+	    moveDownElement(); 
+	}
+       
+    }
+
+     
+    public void addElement(boolean subelement){
+	TreePath tse = self.getSelectionPath();
+	if (tse == null) return;
+	
+	DefaultTreeModel dtm = (DefaultTreeModel)getModel();
+	DefaultMutableTreeNode snode=(DefaultMutableTreeNode)tse.getLastPathComponent();
+	Object data = snode.getUserObject();
+	if (data instanceof MetadataSetInfo){
+	    data = metadata_model;
+	}
+	NewMetadataElementNamePrompt name_prompt = new NewMetadataElementNamePrompt(parent_frame, subelement, data);
+	if (name_prompt.isCancelled()) {
+	    return;
+	}
+	String new_name = name_prompt.getName();
+	
+	MetadataElementModel element_model = new MetadataElementModel(metadata_model.getMetadataSetInfo(), new_name);
+	DefaultMutableTreeNode new_node = new DefaultMutableTreeNode(element_model);
+	dtm.insertNodeInto(new_node,snode,snode.getChildCount());
+	//update model
+	if (data instanceof MetadataElementModel){
+	    ((MetadataElementModel) data).addChild(element_model);
+	}
+	else if(data instanceof MetadataSetModel){
+	    ((MetadataSetModel)data).addChild(element_model);
+		
+	}
+	
+	
+	dtm.reload(snode);
+	expandPath(tse);
+	tse = tse.pathByAddingChild(new_node);
+	self.setSelectionPath(tse);
+	element_model.notifyListeners(false); 
+    }
+    
+    
+
+    public void deleteElement(){
+	
+	//int result = JOptionPane.showConfirmDialog(null, Dictionary.get("GEMS.Confirm_DeleteElement", "(Put name here)"), Dictionary.get("GEMS.Confirm_DeleteElement_Title"), JOptionPane.OK_CANCEL_OPTION);
+	//if (result != JOptionPane.OK_OPTION) return;
+
+	TreePath tse = self.getSelectionPath();
+       	 
+	if (tse!=null){
+	    DefaultTreeModel dtm = (DefaultTreeModel)getModel();
+	    DefaultMutableTreeNode snode=(DefaultMutableTreeNode)tse.getLastPathComponent();
+	    DefaultMutableTreeNode parent = (DefaultMutableTreeNode) snode.getParent();
+	    snode.removeFromParent();
+            
+	    Object parent_data = parent.getUserObject();
+	    Object data = snode.getUserObject();
+	    MetadataElementModel selected_model =  null;
+	    if (parent_data instanceof MetadataElementModel){
+		MetadataElementModel parent_model = (MetadataElementModel) parent_data;
+		selected_model = (MetadataElementModel) data; 
+		parent_model.removeChild(selected_model);
+		parent_model.notifyListeners(false);
+	    }
+	    else{
+		if(parent_data instanceof MetadataSetInfo){
+		    selected_model = (MetadataElementModel) data; 
+		    metadata_model.removeChild(selected_model);
+		    ((MetadataSetInfo)parent_data).notifyListeners();
+		    
+		}
+	    }
+	     
+	    dtm.reload(parent);
+	    self.setSelectionPath(tse.getParentPath());
+	    //selected_model.notifyListeners(true);// notify its listeners that it's been deleted
+	}  
+
+    } 
+
+    private void moveUpElement(){
+	TreePath tse = self.getSelectionPath();
+        
+	if (tse!=null){
+	    DefaultMutableTreeNode snode= (DefaultMutableTreeNode)tse.getLastPathComponent();
+	    int row =  getSelectionRows()[0];                      
+	    boolean moved = false;
+
+	    Object data = snode.getUserObject();
+	    
+	    if (data instanceof MetadataElementModel){
+		MetadataElementModel element_model = (MetadataElementModel)data;
+                //top level element
+                if (element_model.getParent() == null){
+		    moved = metadata_model.moveUp(element_model);
+                 
+		}
+                else{
+		    MetadataElementModel parent = element_model.getParent();
+		    moved = parent.moveUp(element_model);                
+		} 
+	       	if (moved){
+                    DefaultTreeModel dtm = (DefaultTreeModel)getModel();
+		    DefaultMutableTreeNode pnode= (DefaultMutableTreeNode) snode.getParent();
+                    int index = pnode.getIndex(snode);
+		    dtm.removeNodeFromParent(snode);
+		    dtm.insertNodeInto(snode,pnode,--index);
+                    dtm.reload(pnode);
+		    setSelectionRow(--row);                         
+		} 
+	    }
+	}
+
+    }
+
+    private void moveDownElement(){
+	TreePath tse = self.getSelectionPath();
+        
+	if (tse!=null){         
+	    boolean moved = false;
+	    DefaultMutableTreeNode snode= (DefaultMutableTreeNode)tse.getLastPathComponent();
+	    int row =  getSelectionRows()[0]; 	    
+	    Object data = snode.getUserObject();
+	    
+	    if (data instanceof MetadataElementModel){
+		MetadataElementModel element_model = (MetadataElementModel)data;
+                //top level element
+                if (element_model.getParent() == null){
+		    moved = metadata_model.moveDown(element_model);
+		}
+                else{
+		    MetadataElementModel parent = element_model.getParent();
+		    moved = parent.moveDown(element_model);                
+		} 
+		// move the node down the tree     
+		if (moved){
+		    DefaultTreeModel dtm = (DefaultTreeModel)getModel();
+		    DefaultMutableTreeNode pnode= (DefaultMutableTreeNode) snode.getParent();
+                    int index = pnode.getIndex(snode);
+		    dtm.removeNodeFromParent(snode);
+		    dtm.insertNodeInto(snode,pnode,++index);
+                    dtm.reload(pnode);
+		    setSelectionRow(++row); 		                         
+		} 
+	    }
+	}
+    }
+    
+
+    private void populateMetadataSetTree(MetadataSetModel metadata_model){
+	setModel(null);
+      	MetadataSetInfo info= metadata_model.getMetadataSetInfo();
+	if (info == null) {
+	    repaint();
+	    
+	    return;
+	}
+
+        ArrayList metadata_elements = metadata_model.getMetadataSetModel();        	 
+
+        DefaultTreeModel dtm = new DefaultTreeModel(new DefaultMutableTreeNode(info));
+	DefaultMutableTreeNode root = (DefaultMutableTreeNode)dtm.getRoot();
+       
+        for(int i=0;i <metadata_elements.size();i++){
+            MetadataElementModel element_model = (MetadataElementModel)metadata_elements.get(i);
+	    DefaultMutableTreeNode element = new DefaultMutableTreeNode(element_model);            
+	    dtm.insertNodeInto(element,root,root.getChildCount());
+            ArrayList subelements = element_model.getChildElements();
+	    for(int j=0;j <subelements.size();j++){
+                MetadataElementModel subelement_model = (MetadataElementModel)subelements.get(j);
+		DefaultMutableTreeNode subelement = new DefaultMutableTreeNode(subelement_model);            
+		dtm.insertNodeInto(subelement,element,element.getChildCount());
+            }
+	} 	 
+	 
+	dtm.reload(root);
+	setModel(dtm);       
+             
+    } 
+
+    private class MetadataSetTreeRightClickMenu
+	extends JPopupMenu {
+
+	private MetadataSetTreeRightClickMenu(MetadataSetTree meta_set_tree, 
+					      MouseEvent event)
+	{
+
+	    super();
+	    
+            // the right click position
+	    boolean root_node = false;
+	    boolean element_node = false;
+	    TreePath root_path = meta_set_tree.getPathForRow(0);
+            TreePath right_click_path = meta_set_tree.getPathForLocation(event.getX(), event.getY());
+            if (right_click_path == null) {
+		right_click_path = root_path;
+		root_node = true;
+	    } else if (right_click_path.equals(root_path)) {
+		root_node = true;
+	    } else if (right_click_path.getParentPath().equals(root_path)) {
+		element_node = true;
+	    } // else its a subelement
+	    
+	    meta_set_tree.clearSelection();
+	    meta_set_tree.setSelectionPath(right_click_path);
+
+	    JMenuItem add_element = new JMenuItem();
+	    if (root_node) {
+		add_element.setText(Dictionary.get("GEMS.Popup.AddElement"));
+		add_element.setActionCommand(GEMSConstants.ADD_ELEMENT);
+	    } else {
+		add_element.setText(Dictionary.get("GEMS.Popup.AddSubElement"));
+		add_element.setActionCommand(GEMSConstants.ADD_SUBELEMENT);
+
+	    }
+	    add_element.addActionListener(meta_set_tree);
+	    add(add_element);
+	    if (!root_node) {
+		// delete element also
+		JMenuItem delete_element = new JMenuItem();
+		if (element_node) {
+		    delete_element.setText(Dictionary.get("GEMS.Popup.DeleteElement"));
+		} else {
+		    delete_element.setText(Dictionary.get("GEMS.Popup.DeleteSubElement"));
+		}
+		delete_element.addActionListener(meta_set_tree);
+		delete_element.setActionCommand(GEMSConstants.DELETE_ELEMENT);
+		add(delete_element);
+	    }
+
+            // Show the popup menu on screen
+            show(meta_set_tree, event.getX(), event.getY());
+        }
+
+    }
+}
+
+
+class MetadataSetCellRenderer extends JLabel implements TreeCellRenderer {
+    
+    private Color SELECTED_COLOR = Color.lightGray;
+    private Color SELECTED_BORDER_COLOR = Color.gray;
+      
+   
+    public Component getTreeCellRendererComponent(JTree tree,
+						  Object value,
+						  boolean selected,
+						  boolean expanded,
+						  boolean leaf,
+						  int row,
+						  boolean hasFocus){
+	
+        DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
+        Object userObject = node.getUserObject();
+
+	if (userObject instanceof MetadataElementModel){
+	    MetadataElementModel model = (MetadataElementModel)userObject;
+	    setText(model.getFullName());
+	}
+	else{
+	    if (userObject instanceof MetadataSetInfo){
+		MetadataSetInfo info = (MetadataSetInfo)userObject;
+                setText(info.getMetadataSetName());	
+	    }
+	}
+   	 
+	if (selected){
+	    setBackground(SELECTED_COLOR);  
+        
+	}
+	else{
+	    setBackground(tree.getBackground());  
+	}
+	setForeground(tree.getForeground());  
+	setOpaque(true);
+	return this;
+    }
+
+} 
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/NewMetadataElementNamePrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/NewMetadataElementNamePrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/NewMetadataElementNamePrompt.java	(revision 31635)
@@ -0,0 +1,151 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Katherine Don, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.ModalDialog;
+
+public class NewMetadataElementNamePrompt
+    extends ModalDialog {
+    private Dimension SIZE = new Dimension(350, 120);
+
+    private boolean cancelled = false;
+    private String element_name = null;
+    private JTextField name_textfield = null;
+    private JDialog prompt;
+    private Object model;
+    public NewMetadataElementNamePrompt(Frame parent, boolean subelement, Object model) {
+	super(parent, true);
+	setSize(SIZE);
+	prompt = this;
+	this.model = model;
+	JPanel content_pane = (JPanel) getContentPane();
+	content_pane.setOpaque(true);
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel name_label = new JLabel();
+	name_label.setComponentOrientation(Dictionary.getOrientation());
+        if (subelement) {
+	    setTitle(Dictionary.get("GEMS.NewMetadataElementNamePrompt.SubTitle"));
+	    name_label.setText(Dictionary.get("GEMS.NewMetadataElementNamePrompt.SubName"));
+	} else {
+	    setTitle(Dictionary.get("GEMS.NewMetadataElementNamePrompt.Title"));
+	    name_label.setText(Dictionary.get("GEMS.NewMetadataElementNamePrompt.Name"));
+	}
+
+	name_textfield = new JTextField();
+        name_textfield.setComponentOrientation(Dictionary.getOrientation());
+	name_textfield.addKeyListener(new KeyAdapter() {
+		public void keyPressed(KeyEvent e) {
+		    if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+			//same as clicking OK button
+			validateElementName();
+		    }
+		}
+	    });
+
+	JPanel details_pane = new JPanel();
+        details_pane.setComponentOrientation(Dictionary.getOrientation());
+	details_pane.setLayout(new GridLayout(2,1));
+	details_pane.add(name_label);
+	details_pane.add(name_textfield);
+	    
+	JPanel button_pane = new JPanel();
+	button_pane.setLayout(new GridLayout(1,2));
+	GLIButton ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));       	
+	GLIButton cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+	button_pane.add(ok_button);
+	button_pane.add(cancel_button);
+	    
+	// Add listeners
+	ok_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    validateElementName();
+		}
+	    });
+	cancel_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    cancelled = true;
+		    prompt.dispose();
+		}
+	    });
+	    
+	content_pane.setLayout(new BorderLayout(5,5));
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.add(details_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+	    
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(true);
+	    
+    }
+
+    public String getName() {
+	return element_name;
+    }
+
+    public boolean isCancelled() {
+	return cancelled;
+    }
+
+    private void validateElementName() {
+	String name = name_textfield.getText();
+	if (name.equals("")) {
+	    JOptionPane.showMessageDialog(prompt,Dictionary.get("GEMS.NewMetadataElementNamePrompt.EmptyName_Error_Message"), Dictionary.get("GEMS.NewMetadataElementNamePrompt.Name_Error"), JOptionPane.ERROR_MESSAGE);
+	    return;
+	} 
+	boolean already_used = false;
+	if (model instanceof MetadataSetModel) {
+	    already_used = ((MetadataSetModel)model).doesChildWithThisNameExist(name);
+	} else if (model instanceof MetadataElementModel) {
+	    already_used = ((MetadataElementModel)model).doesChildWithThisNameExist(name);
+	}
+	if (!already_used) {
+	    element_name = name;
+	    prompt.dispose();
+	} else {
+	    JOptionPane.showMessageDialog(prompt,Dictionary.get("GEMS.NewMetadataElementNamePrompt.Name_Error_Message"), Dictionary.get("GEMS.NewMetadataElementNamePrompt.Name_Error"), JOptionPane.ERROR_MESSAGE);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/NewMetadataSetPrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/NewMetadataSetPrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/NewMetadataSetPrompt.java	(revision 31635)
@@ -0,0 +1,315 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Collection;
+import java.util.Vector;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.gui.ModalDialog;
+import org.greenstone.gatherer.gui.GLIButton;
+
+public class NewMetadataSetPrompt 
+    extends ModalDialog {
+
+    static private Dimension SIZE = new Dimension(500, 320);
+        
+    private ArrayList available_metadata_sets;
+    private ArrayList listeners; 
+
+    private JButton ok_button = null;
+    private JButton cancel_button = null;    
+    private JComboBox base_metadata_combo;
+    private NewMetadataSetPrompt self;
+    private MetadataSetManager meta_manager;
+    private JTextArea description_textarea = null;
+    private JTextField title_field;
+    private JTextField namespace_field;
+
+    private boolean cancelled = false;
+    
+    public NewMetadataSetPrompt(Frame parent,MetadataSetManager msm) {
+	super(parent, true);
+        self = this;
+	listeners = new ArrayList();
+	meta_manager = msm;
+
+	setSize(SIZE);
+	setTitle(Dictionary.get("GEMS.NewMetadataSetPrompt.Title"));
+	
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setOpaque(true);
+	content_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel  instruction_label = new JLabel(Dictionary.get("GEMS.NewMetadataSetPrompt.Instructions"));
+	instruction_label.setOpaque(true);
+	instruction_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel  title_label = new JLabel(Dictionary.get("GEMS.NewMetadataSetPrompt.Metadata_Title"));
+	title_label.setOpaque(true);
+        title_label.setComponentOrientation(Dictionary.getOrientation());
+                 
+        title_field = new JTextField();
+        title_field.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel title_pane = new JPanel(new BorderLayout(5,5));
+        title_pane.setComponentOrientation(Dictionary.getOrientation());
+        title_pane.add(title_label,BorderLayout.LINE_START);
+        title_pane.add(title_field, BorderLayout.CENTER);
+
+	
+	JLabel  namespace_label = new JLabel(Dictionary.get("GEMS.NewMetadataSetPrompt.Metadata_Namespace"));
+        namespace_label.setComponentOrientation(Dictionary.getOrientation());
+ 	namespace_label.setOpaque(true);
+                 
+        namespace_field = new JTextField();
+        namespace_field.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel namespace_pane = new JPanel(new BorderLayout(5,5));
+        namespace_pane.setComponentOrientation(Dictionary.getOrientation());
+        namespace_pane.add(namespace_label,BorderLayout.LINE_START);
+        namespace_pane.add(namespace_field, BorderLayout.CENTER);
+
+        
+        JPanel info_pane = new JPanel();
+        info_pane.setComponentOrientation(Dictionary.getOrientation());
+        info_pane.setLayout(new BorderLayout(5,5));
+        info_pane.add(instruction_label,BorderLayout.NORTH);
+	info_pane.add(title_pane,BorderLayout.CENTER);
+        info_pane.add(namespace_pane,BorderLayout.SOUTH);
+        
+
+	JLabel description_label = new JLabel(Dictionary.get("GEMS.Set_Description"));
+	description_label.setOpaque(true);
+        description_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	description_textarea = new JTextArea();
+        description_textarea.setComponentOrientation(Dictionary.getOrientation());
+        description_textarea.setLineWrap(true); 
+        description_textarea.setWrapStyleWord(true);
+
+	JPanel description_pane = new JPanel();
+        description_pane.setComponentOrientation(Dictionary.getOrientation());
+        description_pane.setLayout(new BorderLayout());
+        description_pane.add(description_label,BorderLayout.NORTH);
+	description_pane.add(new JScrollPane(description_textarea),BorderLayout.CENTER);
+
+       
+	JLabel  base_label = new JLabel(Dictionary.get("GEMS.NewMetadataSetPrompt.Base_MetadataSet"));
+	base_label.setOpaque(true);
+        base_label.setComponentOrientation(Dictionary.getOrientation());
+        
+        base_metadata_combo = new JComboBox();
+	base_metadata_combo.setRenderer(new MetadatSetListCellRenderer());
+	base_metadata_combo.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel base_pane = new JPanel(new BorderLayout(5,5));
+        base_pane.setComponentOrientation(Dictionary.getOrientation());
+        base_pane.add(base_label,BorderLayout.LINE_START);
+        base_pane.add(base_metadata_combo, BorderLayout.CENTER);
+
+
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));       	
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+      
+
+	// Add listeners
+	ok_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    if (createNewSet()) {
+			self.dispose();
+		    } // else if that returned false, then we leave the
+		    // prompt there for them to change their input
+		}
+	    });
+
+				    
+	cancel_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    cancelled = true;
+		    self.dispose();
+		}
+	    });
+	
+ 
+	button_pane.setLayout(new GridLayout(1,2));
+	button_pane.add(ok_button);
+	button_pane.add(cancel_button);
+	
+	JPanel bottom_pane = new JPanel(new GridLayout(2,1,5,5));
+	bottom_pane.add(base_pane);
+	bottom_pane.add(button_pane);
+	
+	content_pane.setLayout(new BorderLayout(5,5));
+	content_pane.add(info_pane, BorderLayout.NORTH);
+	content_pane.add(description_pane, BorderLayout.CENTER);
+	content_pane.add(bottom_pane, BorderLayout.SOUTH);
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	
+	// Show
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(false);
+	
+    }
+    
+    
+    public void display(){
+	cancelled = false;
+	available_metadata_sets =  meta_manager.getAvailableMetadataSets();
+	Vector data = new Vector((Collection)available_metadata_sets);
+	data.add(0,Dictionary.get("GEMS.NewMetadataSetPrompt.New_Metadata"));
+	DefaultComboBoxModel  model = new DefaultComboBoxModel(data);
+	title_field.setText("");
+	namespace_field.setText("");
+	description_textarea.setText("");
+	base_metadata_combo.setModel(model); 
+	setVisible(true);          
+    }
+
+    public boolean isCancelled() {
+	return cancelled;
+    }
+    
+    public void addMetadataSetListener(MetadataSetListener msl){
+	listeners.add(msl);
+   
+    }
+
+    private boolean createNewSet() {
+
+	String title = title_field.getText();
+	String namespace = namespace_field.getText();
+	String description = description_textarea.getText();
+
+	if (title == null || title.trim().equals("")){
+	    JOptionPane.showMessageDialog(self, Dictionary.get("GEMS.NewMetadataSetPrompt.Title_Error_Message"), Dictionary.get("GEMS.NewMetadataSetPrompt.Title_Error"), JOptionPane.ERROR_MESSAGE);
+
+	    return false;
+	}
+
+	if (namespace == null || namespace.trim().equals("")){
+	    JOptionPane.showMessageDialog(self, Dictionary.get("GEMS.NewMetadataSetPrompt.Namespace_Error_Message"), Dictionary.get("GEMS.NewMetadataSetPrompt.Namespace_Error"), JOptionPane.ERROR_MESSAGE);
+	    return false;
+	}
+
+	//check namespace conflict
+	if (meta_manager.isNamespaceAlreadyUsed(namespace)) {
+	    int result = JOptionPane.showOptionDialog(null,Dictionary.get("GEMS.Namespace_Conflict_Message"), Dictionary.get("GEMS.Namespace_Conflict"),JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE,null,GEMSConstants.DIALOG_OPTIONS,GEMSConstants.DIALOG_OPTIONS[0] );
+	     
+	    if (result != JOptionPane.OK_OPTION) return false;
+	}
+
+	Object selectedValue = base_metadata_combo.getSelectedItem();
+        
+	if (selectedValue != null ){
+	    MetadataSetInfo meta_info = null;
+               
+	    if ((selectedValue instanceof MetadataSetInfo)){	  
+		meta_info = (MetadataSetInfo)selectedValue;
+	    }
+	    else{
+		meta_info = new MetadataSetInfo();                	
+	    }
+                
+	    meta_info.setNew(true);
+	    // clear all the language dependent attributes
+	    meta_info.setLanguageDependentAttributes(new ArrayList());
+	    meta_info.setMetadataSetName(title); 
+	    meta_info.setMetadataSetDescription(description);
+	    meta_info.setNamespace(namespace);
+	    meta_info.setCurrentLanguage(meta_manager.getCurrentLanguage());
+	    notifyListeners(meta_info);
+	}
+	return true;
+    }
+
+ 
+    private void notifyListeners(MetadataSetInfo set_info){
+	MetadataSetEvent mse = new MetadataSetEvent(set_info);
+	for(int i=0;i<listeners.size();i++){
+	    MetadataSetListener msl = (MetadataSetListener)listeners.get(i);
+	    msl.metadataSetChanged(mse);          
+	}	
+    }
+
+    private class MetadatSetListCellRenderer extends JLabel 
+	implements ListCellRenderer {
+	public  MetadatSetListCellRenderer() {
+	    setOpaque(true);
+	}
+	
+	public Component getListCellRendererComponent(JList list,
+						      Object value,
+						      int index,
+						      boolean isSelected,
+						      boolean cellHasFocus)
+	{
+	    String name= "unknown";
+
+	    if (value instanceof MetadataSetInfo){
+		MetadataSetInfo meta_info = (MetadataSetInfo) value;
+		name = meta_info.getMetadataSetName(); //get the name of metadata set
+		setText(name);
+	    }
+	    else {
+		setText(value.toString());
+	    }
+
+	    if (isSelected) {
+		setBackground(list.getSelectionBackground());
+		setForeground(list.getSelectionForeground());
+	    }
+	    else {
+		setBackground(list.getBackground());
+		setForeground(list.getForeground());
+	    }
+	              
+	    return this;
+	}
+    }
+       
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/OpenMetadataSetPrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/OpenMetadataSetPrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gems/OpenMetadataSetPrompt.java	(revision 31635)
@@ -0,0 +1,283 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Shaoqun Wu, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gems;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Collection;
+import java.util.Vector;
+import java.io.File;
+
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.gui.ModalDialog;
+import org.greenstone.gatherer.gui.GLIButton;
+
+public class OpenMetadataSetPrompt 
+    extends ModalDialog {
+
+    static private Dimension SIZE = new Dimension(500, 500);
+        
+    private ArrayList available_metadata_sets;
+    private ArrayList listeners; 
+
+    private JButton open_button = null;
+    private JButton cancel_button = null; 
+    private JButton browse_button = null; 
+    private JList  available_set_list = null;
+    private OpenMetadataSetPrompt self;
+    private MetadataSetManager meta_manager;
+    private JTextArea description_textarea = null;
+
+    private boolean cancelled = false;
+    
+    public OpenMetadataSetPrompt(Frame parent,MetadataSetManager msm) {
+	super(parent, true);
+        
+        self = this;
+	meta_manager = msm;
+	setSize(SIZE);
+	setTitle(Dictionary.get("GEMS.OpenMetadataSetPrompt.Title"));
+	listeners = new ArrayList();
+
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setOpaque(true);
+	content_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel  available_metadata_sets_label = new JLabel(Dictionary.get("GEMS.OpenMetadataSetPrompt.Available_Sets"));
+	available_metadata_sets_label.setOpaque(true);
+        available_metadata_sets_label.setComponentOrientation(Dictionary.getOrientation());
+        
+        available_set_list = new JList();
+	available_set_list.setCellRenderer(new MetadatSetListCellRenderer());
+	available_set_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        available_set_list.setFixedCellHeight(20);
+	available_set_list.addListSelectionListener(new MetadataSetListSelectionListener());
+        available_set_list.setComponentOrientation(Dictionary.getOrientation());
+        
+        JPanel set_pane = new JPanel();
+        set_pane.setComponentOrientation(Dictionary.getOrientation());
+        set_pane.setLayout(new BorderLayout());
+        set_pane.add(available_metadata_sets_label,BorderLayout.NORTH);
+	set_pane.add(new JScrollPane(available_set_list),BorderLayout.CENTER);
+        
+	JLabel  metadata_set_des_label = new JLabel(Dictionary.get("GEMS.Set_Description"));
+	metadata_set_des_label.setOpaque(true);
+        metadata_set_des_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	description_textarea = new  JTextArea();
+        description_textarea.setOpaque(true);
+        description_textarea.setEditable(false);
+        description_textarea.setLineWrap(true); 
+        description_textarea.setWrapStyleWord(true);
+        description_textarea.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel des_pane = new JPanel();
+        des_pane.setLayout(new BorderLayout());
+        des_pane.add(metadata_set_des_label,BorderLayout.NORTH);
+	des_pane.add(new JScrollPane(description_textarea),BorderLayout.CENTER);
+        des_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	open_button = new GLIButton(Dictionary.get("GEMS.OpenMetadataSetPrompt.Open"), Dictionary.get("GEMS.OpenMetadataSetPrompt.Open_Tooltip"));
+	open_button.setEnabled(true);	
+        
+	browse_button = new GLIButton(Dictionary.get("General.Browse"));
+        browse_button.setEnabled(true);
+
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+	cancel_button.setEnabled(true);
+
+
+	// Add listeners
+	open_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    notifyListeners();
+		    self.dispose();
+		}
+	    });
+
+	browse_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    String collect_dir = meta_manager.getCollectionPath();
+		    JFileChooser file_chooser =  new JFileChooser(new File(collect_dir));
+		    file_chooser.setMultiSelectionEnabled(false);
+		    int result = file_chooser.showOpenDialog(null);
+		    if (result == JFileChooser.APPROVE_OPTION){
+			String file_path = file_chooser.getSelectedFile().toString();
+			MetadataSetInfo meta_info = meta_manager.getMetadataSet(file_path);
+			meta_info.setNew(false);
+			MetadataSetEvent mse = new MetadataSetEvent(meta_info);
+			for(int i=0;i<listeners.size();i++){
+			    MetadataSetListener msl = (MetadataSetListener)listeners.get(i);
+			    msl.metadataSetChanged(mse);          
+			}
+			
+		    }     
+		    notifyListeners();
+		    self.dispose();
+		}
+	    });
+
+
+
+	cancel_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    cancelled = true;
+		    self.dispose();
+		}
+	    });
+
+
+	button_pane.setLayout(new GridLayout(1,3));
+	button_pane.add(open_button);
+	button_pane.add(browse_button);
+	button_pane.add(cancel_button);
+	
+
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(set_pane, BorderLayout.NORTH);
+	content_pane.add(des_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+	
+	// Show
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(false);
+
+    }
+
+
+    public void display(){
+	cancelled = false;
+        available_metadata_sets = meta_manager.getAvailableMetadataSets();
+	available_set_list.setListData(new Vector((Collection)available_metadata_sets)); 
+        setVisible(true);          
+    }
+
+    public void addMetadataSetListener(MetadataSetListener msl){
+        listeners.add(msl);
+   
+    }
+
+    public boolean isCancelled() {
+	return cancelled;
+    }
+
+    private void notifyListeners(){
+	Object selectedValue = available_set_list.getSelectedValue();
+	
+	if (selectedValue !=null && (selectedValue instanceof MetadataSetInfo)){
+	    MetadataSetEvent mse = new MetadataSetEvent((MetadataSetInfo)selectedValue);
+	    for(int i=0;i<listeners.size();i++){
+		MetadataSetListener msl = (MetadataSetListener)listeners.get(i);
+		msl.metadataSetChanged(mse);          
+	    }
+	}
+    }
+
+    public void openMetadataSet(String metadata_path){
+       MetadataSetInfo set_info = meta_manager.getMetadataSet(metadata_path);                       
+	notifyListeners(set_info);   
+    }
+
+    private void notifyListeners(MetadataSetInfo set_info){
+	MetadataSetEvent mse = new MetadataSetEvent(set_info);
+	for(int i=0;i<listeners.size();i++){
+	    MetadataSetListener msl = (MetadataSetListener)listeners.get(i);
+	    msl.metadataSetChanged(mse);          
+	}	
+    }
+
+    private class MetadatSetListCellRenderer extends JLabel implements ListCellRenderer {
+	public  MetadatSetListCellRenderer() {
+	    setOpaque(true);
+	}
+	
+     public Component getListCellRendererComponent(
+						   JList list,
+						   Object value,
+						   int index,
+						   boolean isSelected,
+         boolean cellHasFocus)
+	{
+	    String name= "unknown";
+
+	    if (value instanceof MetadataSetInfo){
+		MetadataSetInfo meta_info = (MetadataSetInfo) value;
+		name = meta_info.getMetadataSetName(); //get the name of metadata set
+	    }
+  
+            setText(name);
+            if (isSelected) {
+		setBackground(list.getSelectionBackground());
+		setForeground(list.getSelectionForeground());
+	    }
+	    else {
+		setBackground(list.getBackground());
+		setForeground(list.getForeground());
+	    }
+	              
+	    return this;
+	}
+    }
+   
+    
+    private class MetadataSetListSelectionListener implements ListSelectionListener  {
+        public void valueChanged(ListSelectionEvent lse){
+	    if (lse.getValueIsAdjusting()) return;
+
+	    Object selectedValue = available_set_list.getSelectedValue();
+
+            if (selectedValue !=null && (selectedValue instanceof MetadataSetInfo)){
+                MetadataSetInfo meta_info = (MetadataSetInfo)selectedValue; 
+		String des = meta_info.getMetadataSetDescription(); //get the description of the metadata set
+                description_textarea.setText(des);
+	    }      
+
+	}
+    }
+   
+ 
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/Classifiers.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/Classifiers.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/Classifiers.java	(revision 31635)
@@ -0,0 +1,302 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.greenstone;
+
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.Argument;
+import org.greenstone.gatherer.cdm.Classifier;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+import org.xml.sax.*;
+
+
+public class Classifiers
+{
+    // A list of all the classifiers in the core Greenstone "perllib/classify" folder (arguments may not be loaded)
+    static private ArrayList core_greenstone_classifiers_list = null;
+    // The name of the loaded collection
+    static private String collection_name = null;
+    // A list of all the classifiers in the loaded collection's "perllib/classify" folder (arguments may not be loaded)
+    static private ArrayList collection_specific_classifiers_list = new ArrayList();
+
+
+    static public Classifier getClassifier(String classifier_name, boolean arguments_required)
+    {
+	Classifier classifier = null;
+	boolean collection_specific = false;
+
+	// Check the collection-specific classifiers first
+	for (int i = 0; i < collection_specific_classifiers_list.size(); i++) {
+	    Classifier collection_specific_classifier = (Classifier) collection_specific_classifiers_list.get(i);
+	    if (collection_specific_classifier.getName().equals(classifier_name)) {
+		classifier = collection_specific_classifier;
+		collection_specific = true;
+		break;
+	    }
+	}
+
+	// Try the core Greenstone classifiers if necessary
+	if (classifier == null) {
+	    for (int i = 0; i < core_greenstone_classifiers_list.size(); i++) {
+		Classifier core_greenstone_classifier = (Classifier) core_greenstone_classifiers_list.get(i);
+		if (core_greenstone_classifier.getName().equals(classifier_name)) {
+		    classifier = core_greenstone_classifier;
+		    break;
+		}
+	    }
+	}
+
+	// If we've found the classifier, load its arguments now, if required
+	if (classifier != null && arguments_required) {
+	    if (classifier.getArguments().size() == 0 && classifier.didLoadingOptionsFail() == false) {
+		loadClassifierInfo(classifier, collection_specific);
+	    }
+	    else {
+		DebugStream.println("Already loaded arguments for " + classifier_name + "!");
+	    }
+	}
+
+	return classifier;
+    }
+
+
+    /** Returns a new list from merging the collection-specific and the core Greenstone classifiers. */
+    static public ArrayList getClassifiersList()
+    {
+	ArrayList classifiers_list = new ArrayList();
+	classifiers_list.addAll(collection_specific_classifiers_list);
+
+	// Add in the core Greenstone classifiers, taking care not to overwrite any collection-specific ones
+	for (int i = 0; i < core_greenstone_classifiers_list.size(); i++) {
+	    Classifier core_greenstone_classifier = (Classifier) core_greenstone_classifiers_list.get(i);
+
+	    boolean found = false;
+	    for (int j = 0; j < collection_specific_classifiers_list.size(); j++) {
+		Classifier collection_specific_classifier = (Classifier) collection_specific_classifiers_list.get(j);
+		if (core_greenstone_classifier.getName().equals(collection_specific_classifier.getName())) {
+		    found = true;
+		    break;
+		}
+	    }
+
+	    if (!found) {
+		classifiers_list.add(core_greenstone_classifier);
+	    }
+	}
+
+	return classifiers_list;
+    }
+
+
+    static private void loadClassifierInfo(Classifier classifier, boolean collection_specific)
+    {
+	DebugStream.println("Loading arguments for " + classifier.getName() + "...");
+
+	// Run classifierfo.pl to get the list of classifiers
+	try {
+	    String classinfo_xml = null;
+	    if (Gatherer.isGsdlRemote) {
+		String classinfo_options = "&classifier=" + classifier;
+		if (collection_specific) {
+		    classinfo_options += "&collection=" + collection_name;
+		}
+		classinfo_xml = Gatherer.remoteGreenstoneServer.getScriptOptions("classinfo.pl", classinfo_options);
+	    }
+	    else {
+		ArrayList args = new ArrayList();
+		args.add(Configuration.perl_path);
+		args.add("-S");		
+		args.add(LocalGreenstone.getBinScriptDirectoryPath() + "classinfo.pl");
+		if (collection_specific) {
+		    args.add("-collection");
+		    args.add(collection_name);
+		}
+		args.add("-xml");
+		args.add("-language");
+		args.add(Configuration.getLanguage());
+		args.add(classifier.getName());
+
+		// Run the classinfo.pl process
+		Runtime runtime = Runtime.getRuntime();
+		Process process = runtime.exec((String[]) args.toArray(new String[] { }));
+		InputStream input_stream = process.getErrorStream();
+		StringBuffer classinfo_xml_buffer = XMLTools.readXMLStream(input_stream);
+		if (classinfo_xml_buffer != null) {
+		    classinfo_xml = classinfo_xml_buffer.toString();
+		}
+	    }
+
+	    // Check the XML output was obtained successfully
+	    if (classinfo_xml == null || classinfo_xml.length() == 0) {
+		classifier.setLoadingOptionsFailed();
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.ClassifierManager.Classifier_XML_Parse_Failed", classifier.getName()), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		return;
+	    }
+
+	    parseClassifierInfoXML(classifier, classinfo_xml);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+
+
+    static public void loadClassifiersList(String collection_name_arg)
+    {
+	DebugStream.println("In loadClassifiersList()...");
+
+	// If we're getting the collection-specific classifiers, clear the old list no matter what
+	if (collection_name_arg != null) {
+	    collection_name = collection_name_arg;
+	    collection_specific_classifiers_list = new ArrayList();
+	}
+
+	// Run classifierfo.pl to get the list of classifiers
+	try {
+	    StringBuffer xml = null;
+	    if (Gatherer.isGsdlRemote) {
+		String classinfo_options = "&listall";
+		if (collection_name != null) {
+		    classinfo_options += "&collection=" + collection_name;
+		}
+		String classinfo_output = Gatherer.remoteGreenstoneServer.getScriptOptions("classinfo.pl", classinfo_options);
+		xml = new StringBuffer(classinfo_output);
+	    }
+	    else {
+		ArrayList args = new ArrayList();
+		args.add(Configuration.perl_path);
+		args.add("-S");
+		args.add(LocalGreenstone.getBinScriptDirectoryPath() + "classinfo.pl");
+		if (collection_name != null) {
+		    args.add("-collection");
+		    args.add(collection_name);
+		}
+		args.add("-listall");
+		args.add("-xml");
+
+		// Run the classinfo.pl process
+		Runtime runtime = Runtime.getRuntime();
+		Process process = runtime.exec((String[]) args.toArray(new String[] { }));
+		InputStream input_stream = process.getErrorStream();
+		xml = XMLTools.readXMLStream(input_stream);
+	    }
+
+	    // Check the XML output was obtained successfully
+	    if (xml == null || xml.length() == 0) {
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.ClassifierManager.Classifier_List_XML_Parse_Failed"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		return;
+	    }
+
+	    if (collection_name != null) {
+		collection_specific_classifiers_list = parseClassifiersListXML(xml.toString());
+	    }
+	    else {
+		core_greenstone_classifiers_list = parseClassifiersListXML(xml.toString());
+	    }
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+
+
+    static private void parseClassifierInfoXML(Classifier classifier, String xml)
+    {
+	Document document = XMLTools.parseXML(new StringReader(xml));
+	if (document == null) {
+	    classifier.setLoadingOptionsFailed();
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.ClassifierManager.Classifier_XML_Parse_Failed", classifier.getName()), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	    return;
+	}
+
+	parseClassifierInfoXMLNode(classifier, document.getDocumentElement());
+    }
+
+
+    static private void parseClassifierInfoXMLNode(Classifier classifier, Node root_node)
+    {
+	for (Node node = root_node.getFirstChild(); node != null; node = node.getNextSibling()) {
+	    String node_name = node.getNodeName();
+
+	    if (node_name.equalsIgnoreCase("Name")) {
+		classifier.setName(XMLTools.getValue(node));
+	    }
+	    else if (node_name.equals("Desc")) {
+		classifier.setDescription(XMLTools.getValue(node));
+	    }
+	    else if (node_name.equals("Abstract")) {
+		classifier.setIsAbstract(XMLTools.getValue(node).equalsIgnoreCase(StaticStrings.YES_STR));
+	    }
+	    // Parse the classifier arguments
+	    else if (node_name.equalsIgnoreCase("Arguments")) {
+		for (Node argument_node = node.getFirstChild(); argument_node != null; argument_node = argument_node.getNextSibling()) {
+		    // An option
+		    if (argument_node.getNodeName().equalsIgnoreCase("Option")) {
+			Argument argument = new Argument();
+			argument.parseXML((Element) argument_node);
+			classifier.addArgument(argument);
+		    }
+		}
+	    }
+	    // A super classifier class
+	    else if (node_name.equalsIgnoreCase("ClassInfo")) {
+		Classifier super_classifier = new Classifier();
+		parseClassifierInfoXMLNode(super_classifier, node);
+		classifier.setSuper(super_classifier);
+	    }
+	}
+    }
+
+
+    static private ArrayList parseClassifiersListXML(String xml)
+    {
+	ArrayList classifiers_list = new ArrayList();
+
+	Document document = XMLTools.parseXML(new StringReader(xml));
+	Node root = document.getDocumentElement();
+	for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) {
+	    String node_name = node.getNodeName();
+
+	    if (node_name.equals("ClassInfo")) {
+		Classifier classifier = new Classifier();
+		parseClassifierInfoXMLNode(classifier, node);
+		classifiers_list.add(classifier);
+	    }
+	}
+
+	return classifiers_list;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/LocalGreenstone.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/LocalGreenstone.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/LocalGreenstone.java	(revision 31635)
@@ -0,0 +1,64 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.greenstone;
+
+
+import java.io.*;
+
+
+/**
+ * This class contains static functions that only make sense when running with a local Greenstone.
+ * None of these functions should be called if the GLI is using a remote Greenstone.
+ */
+public class LocalGreenstone
+{
+    static private String gsdl_directory_path = null;
+
+
+    static public String getBinScriptDirectoryPath()
+    {
+	return gsdl_directory_path + "bin" + File.separator + "script" + File.separator;
+    }
+
+
+    static public String getDirectoryPath()
+    {
+	return gsdl_directory_path;
+    }
+
+
+    static public String getTmpDirectoryPath()
+    {
+	return gsdl_directory_path + "tmp" + File.separator;
+    }
+
+
+    static public void setDirectoryPath(String gsdl_directory_path_arg)
+    {
+	gsdl_directory_path = gsdl_directory_path_arg;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/LocalLibraryServer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/LocalLibraryServer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/LocalLibraryServer.java	(revision 31635)
@@ -0,0 +1,1041 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.greenstone;
+
+
+import java.io.*;
+import java.lang.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.PortFinder;
+import org.greenstone.gatherer.util.Utility;
+
+
+public class LocalLibraryServer
+{
+    static final private int WAITING_TIME = 20; // number of seconds to wait for the server to start and stop
+
+    static final private String ADD_COMMAND = "?a=config&cmd=add-collection&c=";
+    static final private String RELEASE_COMMAND = "?a=config&cmd=release-collection&c=";
+    static final private String QUIT_COMMAND = "?a=config&cmd=kill";
+	static final private String RESTART_COMMAND = "?a=config&cmd=restart";
+
+    static private LLSSiteConfig llssite_cfg_file = null;
+    static private File local_library_server_file = null;
+
+    static private boolean running = false;
+    static private String ID = "greenstone-server"; // a sort of process ID
+
+    // Need to use sockets to tell the server program to terminate when on Linux
+    // The socket port number that we will use to communicate the termination
+    static private int port;
+    static private Socket clientSocket = null;
+    static private Writer clientSocketWriter = null;
+
+    // The server is persistent if it does not have to reload all the values
+    // over and over again each time. Tomcat is persistent and fastcgi is too,
+    // but the Apache webserver is not persistent by default.
+    // Change the initialisation of this value depending on whether fastcgi is 
+    // on. At the moment, this does not apply to the Apache local library server.
+    static private boolean isPersistentServer = false;
+
+	static public boolean isPersistentServer() {
+		return isPersistentServer;
+	}
+	
+    static public void addCollection(String collection_name)
+    {
+	if (isPersistentServer) {
+	    config(ADD_COMMAND + collection_name);
+	}
+    }
+
+
+    // Used to send messages to the local library
+    static private void config(String command)
+    {
+	if (!isPersistentServer) {
+	    return;
+	}
+	if (Configuration.library_url == null) {
+	    System.err.println("Error: Trying to configure local library with null Configuration.library_url!");
+	    return;
+	}
+
+	try {
+	    URL url = new URL(Configuration.library_url.toString() + command);
+	    HttpURLConnection library_connection = (HttpURLConnection) url.openConnection();
+
+	    // It's very important that we read the output of the command
+	    // This ensures that the command has actually finished
+	    // (The response code is returned immediately)
+	    InputStream library_is = library_connection.getInputStream();
+	    BufferedReader library_in = new BufferedReader(new InputStreamReader(library_is, "UTF-8"));
+	    String library_output_line = library_in.readLine();
+	    while (library_output_line != null) {
+		DebugStream.println("Local library server output: " + library_output_line);
+		library_output_line = library_in.readLine();
+	    }
+	    library_in.close();
+
+	    int response_code = library_connection.getResponseCode();
+	    if (response_code >= HttpURLConnection.HTTP_OK && response_code < HttpURLConnection.HTTP_MULT_CHOICE) {
+		DebugStream.println("200 - Complete.");
+	    }
+	    else {
+		DebugStream.println("404 - Failed.");
+	    }
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+
+    // Used to send messages to the local library server wrapper to the Apache web server (server.jar)
+    static private boolean sendMessageToServer(String message) {
+	if(isPersistentServer) {
+	    return false;
+	}
+	
+	if(port == -1) {
+	    return false;
+	}
+	    
+	try {
+	    if(clientSocket == null) {
+		clientSocket = new Socket("localhost", port);
+	    }
+	    if(clientSocketWriter == null) {
+		clientSocketWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
+	    }
+	    clientSocketWriter.write(message);
+	    clientSocketWriter.flush();
+	    
+	} catch (Exception e) {
+	    System.err.println("An exception occurred when trying to send the message: " + message 
+			       + "\nto the LocalLibraryServer.\n" + e);
+	    return false;
+	} 
+	return true;
+    }
+
+    static public boolean isRunning()
+    {
+	if (!running) return false; // if the url is pending, then running would also be false (server not started up yet)
+
+	llssite_cfg_file.load(true);
+	String url = llssite_cfg_file.getURL();
+	if (url == null) return false;
+	
+	// Called by Gatherer to check whether we need to stop the server.
+	// if the url is pending, then the GSI hasn't started the server up yet
+	// Already covered in !running
+	//if (url.equals(LLSSiteConfig.URL_PENDING)) return false;
+
+	return true;
+    }
+
+	/** collection name can be a group-qualified collect name */
+    static public void releaseCollection(String collection_name)
+    {
+	if (isPersistentServer) {
+	    config(RELEASE_COMMAND + collection_name);
+	}
+    }
+
+    static public boolean start(String gsdl_path, String local_library_server_file_path)
+    {
+	// Check the local library server file (server.exe or gs2-server.sh) exists
+	if(local_library_server_file_path == null) {
+	    return false;
+	}
+	local_library_server_file = new File(local_library_server_file_path);
+	if (local_library_server_file.exists()) {
+	    if(local_library_server_file.getName().equals("server.exe")) {
+		isPersistentServer = true;
+	    } // else it may be gs2-web-server.bat
+	}
+	else { // local_library_server_file does not exist
+	    DebugStream.println("No local library at given file path.");
+
+	    if(Utility.isWindows()) { // test for server.exe and gs2-web-server.bat
+		local_library_server_file = new File(gsdl_path + "server.exe");
+		if (local_library_server_file.exists()) {
+		    isPersistentServer = true;
+		} else {
+		    local_library_server_file = new File(gsdl_path + "gs2-web-server.bat");
+		    if (!local_library_server_file.exists()) {
+			DebugStream.println("No local library at all.");
+			return false;
+		    }
+		}		
+	    } else { // linux
+		local_library_server_file = new File(gsdl_path + "gs2-server.sh");
+		if (!local_library_server_file.exists()) {
+		    DebugStream.println("No local library at all.");
+		    return false;
+		}
+	    }
+	}	
+
+	if(!isPersistentServer) {
+	    // In the case of the Local Library Server on Linux or on Win where there's no server.exe, do an extra test:
+	    // If GS2 was not configured with --enable-apache-httpd, then there is no apache webserver folder,
+	    // even though the gs2-server.sh file would still be there. That means if the folder is absent 
+	    // we still have no local library server.
+
+	    File localServerFolder = new File(gsdl_path, "apache-httpd");
+	    if (!localServerFolder.exists() && !localServerFolder.isDirectory()) {
+		DebugStream.println("The web server does not exist at " 
+			    + localServerFolder.getAbsolutePath() + "\nNo local library at all. Trying web library");
+		return false;
+	    } // else apache-httpd folder exists
+	}
+
+	llssite_cfg_file = new LLSSiteConfig(local_library_server_file);
+	if(!llssite_cfg_file.isConfigFileSet()) {
+	    return false;
+	}
+
+	// from now on return true: we're in local_library_mode (even if the server is not running)
+
+	
+	// If the user launched the GSI independent of GLI, but user has not pressed
+	// Enter Library yet, then we will obtain the previewURL later.
+	if(LocalLibraryServer.isURLPending()) {
+	    // running is still false when the URL is pending because only GSI is running, not the server
+	    return true;
+	} else if(llssite_cfg_file.isIndependentGSI()) {
+	    // There is already a url and it's not pending: meaning the server 
+		// has started up and that GSI was launched outside of GLI 
+	    running = true;
+	    return true;    
+	}
+
+	// Spawn local library server process
+	final String QUOTES = Utility.isWindows() ? "\"" : ""; // need to embed path in quotes on Windows for spaces (interferes on Linux)
+	String local_library_server_command = QUOTES+local_library_server_file.getAbsolutePath()+QUOTES + getExtraLaunchArguments(llssite_cfg_file);
+	if(Utility.isWindows() && !isPersistentServer) { // launching gs2-web-server.bat (Windows) needs cmd start
+	    local_library_server_command = "cmd.exe /c start \"GSI\" " + local_library_server_command;
+	}
+
+	// Check if the server is already running
+	String url = llssite_cfg_file.getURL();
+	if (url != null) {
+	    // If it is already running then set the Greenstone web server address and we're done
+	    // E.g. if previously GLI was not properly shut down, the URL property (signifying 
+	    // the server is still running) would still be in the config file.
+	    try {
+		Configuration.library_url = new URL(url);
+		running = true;
+
+		// Run the server interface
+		Gatherer.spawnApplication(local_library_server_command, ID);
+		return true;
+	    }
+	    catch (MalformedURLException exception) {
+		DebugStream.printStackTrace(exception);
+	    }
+	}	
+
+	// Configure the server for immediate entry
+	//llssite_cfg_file.set();
+
+	// Spawn local library server process
+	Gatherer.spawnApplication(local_library_server_command, ID);
+	
+	// Wait until program has started
+	try {
+	    //System.err.println("**** testing server...");
+	    testServerRunning(); // will set running = true when the server is up and running successfully
+	    //System.err.println("**** Is server running: " + running);
+	} catch (IOException bad_url_connection) {
+	    try {
+		// If this fails then we try changing the url to be localhost
+		Configuration.library_url = new URL(llssite_cfg_file.getLocalHostURL());
+		DebugStream.println("Try connecting to server on local host: '" + Configuration.library_url + "'");
+		URLConnection connection = Configuration.library_url.openConnection();
+		connection.getContent();
+		running = true;
+		
+	    } catch (IOException worse_url_connection) {
+		DebugStream.println("Can't connect to server on either address.");
+		Configuration.library_url = null;
+		running = false;
+	    }
+	}
+
+	return true;
+    }
+
+    /** Call this when the collect directory has changed. Only works when using 
+     * the web server launched by GLI. */
+    static public void reconfigure() {
+	if(isPersistentServer) {				
+		// 1st: server may have nulled URL if server was stopped inbetween, 
+		// so load that new URL, if GLI conf file modified
+		// Then put the new collectDir into the GLI configfile being used
+		llssite_cfg_file.load(true);		
+		
+		String collectDir = Gatherer.getCollectDirectoryPath();
+		collectDir = collectDir.substring(0, collectDir.length()-1); // remove file separator at end
+		llssite_cfg_file.put(LLSSiteConfig.COLLECTHOME, collectDir);		    
+		llssite_cfg_file.save();
+		
+		// tell the server to restart, so it will read the new configuration
+		
+		// (1) need a server to be running in order for us to send a restart message to it
+		if(checkServerRunning()) { // if true, it wasn't running before, but has now restarted the server. We're done
+			return; 
+		}
+		// (2) otherwise the server may already have been running, need to tell it to restart after setting the collect dir
+		if(running) {
+			config(RESTART_COMMAND);		
+		}		
+	    return;
+	}
+
+	// can't control the GSI/server if it was launched independent of GLI
+	if(llssite_cfg_file.isIndependentGSI()) {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("Server.Reconfigure"), Dictionary.get("General.Reconfigure"), JOptionPane.INFORMATION_MESSAGE);
+	    return;
+	}
+
+	// someone may have closed the web server
+	if(checkServerRunning()) { // need a server to be running in order for us to send a reconfigure message to it
+	    return; // it has reconfigured and restarted the server
+	}
+
+	// If it was already running, tell the server a reconfigure is required on next restart
+	// and then restart
+	if(running && sendMessageToServer("RECONFIGURE\n")) {
+	    // restart
+	    if(running) {
+		running = false;
+
+		if(sendMessageToServer("RESTART\n")) { //if(sendMessageToServer("RECONFIGURE\n")) {
+		    // wait for the server to update the URL in the gli config file
+		    llssite_cfg_file.setLastModified();
+		    
+		    int attempt_count = 0;
+		    while (!llssite_cfg_file.isModified()) {
+			new OneSecondWait();  // Wait one second (give or take)
+			attempt_count++;
+			
+			// After waiting for the specified time, ask the user whether they want to wait for that long again
+			if (attempt_count == WAITING_TIME) {
+			    break; // can't be waiting forever, we'll be waiting again below
+			}
+		    }		    
+
+		    try {		
+			testServerRunning(); // will set running = true when the server is up and running successfully
+		    } catch (IOException bad_url_connection) {
+			try {
+			    // If this fails then we try changing the url to be localhost
+			    Configuration.library_url = new URL(llssite_cfg_file.getLocalHostURL());
+			    DebugStream.println("Try connecting to server on local host: '" + Configuration.library_url + "'");
+			    URLConnection connection = Configuration.library_url.openConnection();
+			    connection.getContent();
+			    running = true;
+			    
+			} catch (IOException worse_url_connection) {
+			    DebugStream.println("Can't connect to server on either address.");
+			    Configuration.library_url = null;
+			    running = false;
+			}
+		    }
+		} 
+	    }
+	} else {
+	    System.err.println("GLI was unable to send a reconfigure request to the local library server."
+		       + "\nPlease reconfigure and restart the local Greenstone server manually.");
+	}
+    }
+
+
+    static public void stop()
+    {
+	if (!running) {
+	    // also the case if the URL is pending in an independently launched GSI
+	    return;
+	}
+
+	// don't (can't) shutdown the GSI/server if it was launched independent of GLI
+	if(llssite_cfg_file.isIndependentGSI()) {
+	    return;
+	}
+
+	// Send the command for it to exit.
+	if (isPersistentServer) {
+	    config(QUIT_COMMAND);
+	} else {		
+	    boolean success = sendMessageToServer("QUIT\n");
+	    try {
+		if(clientSocketWriter != null) {
+		    clientSocketWriter.close();
+		    clientSocketWriter = null;
+		}
+		clientSocket = null;
+	    } catch(Exception e) {
+		System.err.println("An exception occurred when trying to close the socket"
+				   + "\nto the LocalLibraryServer.\n" + e);
+	    }
+	    if(success) {
+		Gatherer.terminateApplication(ID);
+	    } else {
+		System.err.println("Unable to stop the server, since there's no communication port to send the quit msg over."
+				   + "\nPlease stop the local Greenstone server manually.");	    
+	    }
+	}
+
+	// Wait until program has stopped, by reloading and checking the URL field
+	llssite_cfg_file.load(false);
+	int attempt_count = 0;
+	String url = llssite_cfg_file.getURL();
+	while (url != null && !url.equals(LLSSiteConfig.URL_PENDING)) { // if pending, the server is already stopped (not running)
+	    new OneSecondWait();  // Wait one second (give or take)
+	    llssite_cfg_file.load(false);
+	    attempt_count++;
+
+	    // After waiting for the specified time, ask the user whether they want to wait for that long again
+	    if (attempt_count == WAITING_TIME) {
+		int try_again = JOptionPane.showConfirmDialog(Gatherer.g_man, Dictionary.get("Server.QuitTimeOut", Integer.toString(WAITING_TIME)),
+							      Dictionary.get("General.Warning"), JOptionPane.YES_NO_OPTION);
+		if (try_again == JOptionPane.NO_OPTION) {
+		    return;
+		}
+		attempt_count = 0;
+	    }
+	    // read the url again to see if it's updated
+	    url = llssite_cfg_file.getURL();
+	}
+
+	// Restore the llssite_cfg.
+	llssite_cfg_file.restore();
+
+	// If the local server is still running then our changed values will get overwritten.
+	url = llssite_cfg_file.getURL();
+	if (url != null && !url.equals(LLSSiteConfig.URL_PENDING)) {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("Server.QuitManual"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	}
+
+	running = false;
+    }
+    
+    static private String getExtraLaunchArguments(LLSSiteConfig site_cfg_file) {
+	String args = " " + LLSSiteConfig.GLIMODE + " " + site_cfg_file.getSiteConfigFilename();
+
+	if(isPersistentServer) {
+	    return args;
+	}
+	
+	// Else, when running the Local Apache Library Server on Linux/Win, need to provide a port argument
+	try {
+	    PortFinder portFinder = new PortFinder(50100, 100);
+	    port = portFinder.findPortInRange(false); // silent mode
+	} catch(Exception e) {
+	    System.err.println("Exception when trying to find an available port: " + e);
+	    port = -1;
+	}
+	
+	return args + " --quitport=" + port;
+    }
+
+
+    // This method first tests whether there is a URL in the llssite_cfg_file 
+    // and after that appears, it tests whether the URL is functional.
+    static private void testServerRunning() throws IOException {
+	// Wait until program has started, by reloading and checking the URL field
+	llssite_cfg_file.load(false);
+	int attempt_count = 0;
+	while (llssite_cfg_file.getURL() == null) {
+	    new OneSecondWait();  // Wait one second (give or take)
+	    //System.err.println("**** One Second Wait");
+	    llssite_cfg_file.load(false);
+	    attempt_count++;
+
+	    // After waiting for the specified time, ask the user whether they want to wait for that long again
+	    if (attempt_count == WAITING_TIME) {
+		int try_again = JOptionPane.showConfirmDialog(Gatherer.g_man, Dictionary.get("Server.StartUpTimeOut", Integer.toString(WAITING_TIME)),
+							      Dictionary.get("General.Warning"), JOptionPane.YES_NO_OPTION);
+		if (try_again == JOptionPane.NO_OPTION) {
+		    return;
+		}
+		attempt_count = 0;
+	    }
+	}
+	
+	// Ta-da. Now the url should be available
+	try {
+	    Configuration.library_url = new URL(llssite_cfg_file.getURL());
+	}
+	catch (MalformedURLException exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+
+	// A quick test involves opening a connection to get the home page for this collection
+	try {
+	    DebugStream.println("Try connecting to server on config url: '" + Configuration.library_url + "'");
+	    URLConnection connection = Configuration.library_url.openConnection();
+	    connection.getContent();
+	    running = true;
+	}
+	catch (IOException bad_url_connection) {
+		System.err.println("Unable to open a connection to " + Configuration.library_url 
+			+ "\nCheck whether the port may already be in use. If so, change your\n"
+			+ "Greenstone port number in the Greenstone Server Interface(GSI).");
+	    throw bad_url_connection;
+	}
+
+	
+    }
+
+    /** Returns true if we're waiting on the user to click on Enter Library button
+     * in an independently launched GSI (one not launched by GLI). */
+    static public boolean isURLPending() {
+	//if(Configuration.library_url != null) {
+	//System.err.println("**** Configuration.library_url: " + Configuration.library_url );
+	//return false; // don't need to do anything as we already have the url
+	//}
+
+	llssite_cfg_file.load(true); // don't force reload, load only if modified
+
+	String url = llssite_cfg_file.getURL();
+
+	if(url != null) {
+	    if(url.equals(LLSSiteConfig.URL_PENDING)) {
+		running = false; // imagine if they restarted an external GSI
+		return true; 
+	    } else {
+		// a valid URL at last
+		try {
+		    Configuration.library_url = new URL(url);
+		    running = true;
+		}
+		catch (MalformedURLException exception) {
+		    running = false;
+		    exception.printStackTrace();
+		    DebugStream.printStackTrace(exception);		    
+		}
+		return false;
+	    }
+	}
+
+	// If either the URL is null--which means no independent GSI (Greenstone server interface
+	// app) was launched--or if the 'URL' doesn't say it's pending, then we're not waiting
+	return false;
+    }
+
+    /** @returns true if it had to restart the server. Returns false if it didn't restart it. */
+    static public boolean checkServerRunning() {
+	boolean serverRestarted = false;
+
+	if (!running) return false; // don't worry about it if it's not supposed to be running 
+	llssite_cfg_file.load(true); // don't force reload, load only if modified
+	
+	String url = llssite_cfg_file.getURL();
+	if(url != null) {
+	    if(url.equals(LLSSiteConfig.URL_PENDING)) {
+		running = false;
+		return false;
+	    }
+
+	    // else, valid URL:
+	    try {
+		Configuration.library_url = new URL(url);
+		running = true;
+	    }
+	    catch (MalformedURLException exception) {
+		running = false;
+		DebugStream.printStackTrace(exception);
+		exception.printStackTrace();
+	    }
+	}
+	else { // NO URL in current mode of configFile, try other mode (e.g. "gli.url")
+		   
+		if(llssite_cfg_file.usingGSImode()) { // if a GSI is already using llssite_cfg, this would have loaded it in
+		url = llssite_cfg_file.getURL(); //if(isURLPending()) {
+		if(url.equals(LLSSiteConfig.URL_PENDING)) {
+		    running = false;
+		} else {
+		    running = true;
+		}
+		return false; // don't need to launch the server, one has been independently launched
+	    } else {
+		// else we try using the "gli." prefix to access the url property in the config file
+		llssite_cfg_file.useGLImode();
+	    	//llssite_cfg_file.set();
+				
+		// since we're going to restart the server, make sure to reset all
+		// the client socket and its writer (for communicating with the web server) 
+		// will only be reinstantiated if they are first nulled
+		if(clientSocket != null) {
+		    clientSocket = null;
+		}
+		if(clientSocketWriter != null) {
+		    try{ 
+			clientSocketWriter.close();
+			clientSocketWriter = null;
+		    } catch(Exception e) {
+			System.err.println("Unable to close the client socket outputstream.");
+		    } finally {
+			clientSocketWriter = null;
+		    }
+		}
+	    
+		// Spawn local library server process
+		running = false;
+		final String QUOTES = Utility.isWindows() ? "\"" : ""; // need to embed path in quotes on Windows for spaces (interferes on Linux)
+		String local_library_server_command = QUOTES+local_library_server_file.getAbsolutePath()+QUOTES + getExtraLaunchArguments(llssite_cfg_file);
+		if(Utility.isWindows() && !isPersistentServer) { // launching gs2-web-server.bat (Windows) needs cmd start
+		    local_library_server_command = "cmd.exe /c start \"GSI\" " + local_library_server_command;
+		}
+		Gatherer.spawnApplication(local_library_server_command, ID);
+		try {
+		    testServerRunning(); // don't return until the webserver is up and running
+		    serverRestarted = true;
+		} catch (IOException bad_url_connection) {
+		    DebugStream.println("Can't connect to server on address " + Configuration.library_url);
+		    running = false;
+		}
+	    }
+	}
+	return serverRestarted;
+    }
+
+    static private class OneSecondWait
+    {
+	public OneSecondWait()
+	{
+	    synchronized(this) {
+		try {
+		    wait(1000);
+		}
+		catch (InterruptedException exception) {
+		}
+	    }
+	}
+    }
+
+
+    static public class LLSSiteConfig
+	extends LinkedHashMap 
+    {
+	private File configFile;
+
+	private String autoenter_initial;
+	private String start_browser_initial;
+
+	private long lastModified = 0;
+    private boolean isIndependentGSI;
+	
+	static final private String GLI_PREFIX = "gli";
+	static final private String GSI_AUTOENTER = "autoenter";
+	static final private String AUTOENTER = GLI_PREFIX+"."+"autoenter";
+	static final private String COLON = ":";
+	static final private String ENTERLIB = "enterlib";
+	static final private String FALSE = "0";
+	static final private String GSDL = "greenstone"; // httpprefix is no longer /gsdl but /greenstone
+	static final private String LLSSITE_CFG = "llssite.cfg";
+	static final private String LOCAL_HOST = "http://localhost"; 
+	static final private String PORTNUMBER = "portnumber";
+	static final private String SEPARATOR = "/";
+	static final private String SPECIFIC_CONFIG = "--config=";
+	static final private String STARTBROWSER = GLI_PREFIX+"."+"start_browser";
+	static final private String GSI_STARTBROWSER = "start_browser";
+	static final private String TRUE = "1";
+	static final private String URL = GLI_PREFIX+"."+"url";
+	static final private String GSI_URL = "url";
+	static final private String COLLECTHOME = "collecthome";
+	static final private String GLIMODE = "--mode="+GLI_PREFIX;
+	
+	static final public String URL_PENDING = "URL_pending";
+
+	
+	public LLSSiteConfig(File server_exe) {
+	    debug("New LLSSiteConfig for: " + server_exe.getAbsolutePath());
+		
+		configFile = new File(server_exe.getParentFile(), LLSSITE_CFG);
+		if(!configFile.exists()) { // create it from the template
+			File llssite_cfg_in = new File(server_exe.getParentFile(), LLSSITE_CFG+".in");
+		
+			// need to generate llssite_cfg from llssite_cfg_in
+			if(llssite_cfg_in.exists()) {
+				copyConfigFile(llssite_cfg_in, configFile, true);   // adjust for gli
+			}
+			else {
+				debug("File llssite.cfg.in can't be found. Can't create llssite.cfg frrom llssite.cfg.in.");				
+			}
+		}			
+		
+		// use the config file now
+	    if(configFile.exists()) {
+			// first test if server was started independently of GLI
+			// if so, the config file we'd be using would be llssite.cfg
+			if(!usingGSImode()) { // this step will try to access the url and other 
+				// special properties in the config file using the "gli." prefix.
+				useGLImode();
+			}
+			
+	    } else {
+			System.err.println("**** ERROR. Configfile is null.");				
+			configFile = null;
+		}
+
+	    autoenter_initial = null;
+	    start_browser_initial = null;
+		
+		// GS2. We're in the constructor, so we'll be running the LLS the first time
+		// Set the correct collectdir (from last time) before the server is started up
+		
+		String orig_collection_path = Configuration.getString("general.open_collection_gs2", true);
+		String collectDir = Gatherer.getCollectDirectoryPath();					// Gatherer would've set this up
+		String defaultColDir = Gatherer.getDefaultGSCollectDirectoryPath(true); // with file separator at end
+				
+		if(orig_collection_path.equals("")) { // default GS collect dir path
+			return;
+		}
+		
+		if (!orig_collection_path.startsWith(collectDir) // if coldir would've been changed on startup OR if coldir is non-standard collect folder
+				|| !collectDir.equals(defaultColDir))
+		{			
+			// if we're running *server.exe* and if the current collect dir at Local Lib's startup is
+			// anything other than the default GS collect dir or if we changed back to the default collect 
+			// dir because the non-standard collectDir didn't exist/wasn't specified in the GLI config
+			// file, then write out the new collectDir (minus file separator at end) to the lls site conf in use
+			// Regardless of what collecthome value was in the llssite file, we end up resetting it here
+			if (isPersistentServer) { // server.exe, so we're dealing with a local GS2			
+				put(COLLECTHOME, collectDir);
+				save();
+				lastModified = configFile.lastModified();
+			}			
+		} // now the correct current collect dir will get loaded when the server is started up hereafter
+	}
+
+	/** Changes the mode to GLI mode (since GSI was not launched independently but by GLI): in this
+	* mode, the "gli." prefix is used to access GLI specific properties from the config file */
+	public void useGLImode()
+	{
+	    isIndependentGSI = false;
+		load(true); // reload if modified 
+	}
+
+	/** Tests whether the server interface is up, running independently of GLI
+	 * If so, we don't need to launch the server interface.
+	 * The server interface may not have started up the server itself though
+	 * (in which case the server URL would be URL_PENDING).
+	 * This method returns true if the server interface has already started
+	 * and, if so, it would have loaded in the configFile. It sets the mode to 
+	 * GSI Mode (meaning GSI was launched independently): no prefix is used to 
+	 * access properties from the config file.
+	 */
+	public boolean usingGSImode() {
+	    // Now to check if the configfile contains the GSI_URL line		
+	    load(false); // force load
+		isIndependentGSI = true;
+	    if(getURL() == null) {
+			isIndependentGSI = false;
+			return false;
+	    }
+
+	    lastModified = configFile.lastModified();
+	    return true;
+	}
+
+	/** To test we've actually instantiated this object meaningfully. If so, then configFile is set */
+	public boolean isConfigFileSet() {
+	    return (configFile != null && configFile.exists());
+	}
+	
+	/** @return true if GSI was started up independently and outside of GLI. 
+	 * In such a case, GLI would be using llssite_cfg. */
+	public boolean isIndependentGSI() {
+	    return isIndependentGSI;
+	}
+
+	public boolean exists() {
+	    return configFile.exists();
+	}
+
+	public String getLocalHostURL() {
+	    StringBuffer url = new StringBuffer(LOCAL_HOST);
+	    url.append(COLON);
+	    url.append((String)get(PORTNUMBER));
+	    String enterlib = (String)get(ENTERLIB);
+	    if(!isPersistentServer || enterlib == null || enterlib.length() == 0) {
+		// Use the default /gsdl and hope for the best.
+		url.append(SEPARATOR);
+		url.append(GSDL);
+	    }
+	    else {
+		if(!enterlib.startsWith(SEPARATOR)) {
+		    url.append(SEPARATOR);
+		}
+		url.append(enterlib);
+	    }
+	    enterlib = null;
+	    debug("Found Local Library Address: " + url.toString());
+	    return url.toString();
+	}
+
+	/** @return the cmd-line parameter for the configfile used to launch
+	 * the server through GLI: --config=<llssite.cfg file path>. */
+	public String getSiteConfigFilename() {
+	    return SPECIFIC_CONFIG + configFile.getAbsolutePath();
+	}
+
+	public String getURL() {
+		String urlPropertyName = isIndependentGSI() ? GSI_URL : URL;
+	    // URL is made from url and portnumber
+	    String url = (String) get(urlPropertyName);
+
+	    // server interface is already up, independent of GLI
+	    // but it has not started the server (hence URL is pending) 
+	    if(url != null && url.equals(URL_PENDING)) {
+		return url;
+	    }
+
+	    if(!isPersistentServer) {
+		return url;
+	    }
+
+	    if(url != null) {
+		StringBuffer temp = new StringBuffer(url);
+		temp.append(COLON);
+		temp.append((String)get(PORTNUMBER));
+		String enterlib = (String)get(ENTERLIB);
+		if(enterlib == null || enterlib.length() == 0) {
+		    // Use the default /greenstone prefix and hope for the best.
+		    temp.append(SEPARATOR);
+		    temp.append(GSDL);
+		}
+		else {
+		    if(!enterlib.startsWith(SEPARATOR)) {
+			temp.append(SEPARATOR);
+		    }
+		    temp.append(enterlib);
+		}
+		enterlib = null;
+		url = temp.toString();
+	    }
+	    debug("Found Local Library Address: " + url);
+	    return url;
+	}
+
+	public void setLastModified() {
+	    if(isModified()) {
+		lastModified = configFile.lastModified();
+	    } 
+	}
+
+	public boolean isModified() {
+	    return (lastModified != configFile.lastModified());
+	}
+
+	public void load(boolean reloadOnlyIfModified) {	    
+
+	    if(configFile == null) {
+		debug(configFile.getAbsolutePath() + " cannot be found!");
+	    }
+
+	    if(isModified()) {
+		lastModified = configFile.lastModified();
+	    } else if(reloadOnlyIfModified) {
+		return; // asked to reload only if modified. Don't reload since not modified
+	    }
+
+	    if(configFile.exists()) {
+		debug("Load: " + configFile.getAbsolutePath());
+		clear();
+		try {
+		    BufferedReader in = new BufferedReader(new FileReader(configFile));
+		    String line = null;
+		    while((line = in.readLine()) != null) {
+			String key = null;
+			String value = null;
+			int index = -1;
+			if((index = line.indexOf("=")) != -1 && line.length() >= index + 1) {
+			    key = line.substring(0, index);
+			    value = line.substring(index + 1);
+			}
+			else {
+			    key = line;
+			}
+			put(key, value);
+		    }
+			
+			in.close();
+		}
+		catch (Exception error) {
+		    error.printStackTrace();
+		}
+	    }
+	    else {
+		debug(configFile.getAbsolutePath() + " cannot be found!");
+	    }
+	}
+
+	/** Restore the autoenter value to its initial value, and remove url if present. */
+	public void restore() {
+	    if(configFile != null) {
+		// Delete the file
+		//configFile.delete();
+		
+		// Not restoring nor deleting here. Just removing the URL (which indicates a running server), 
+		// so that the file's state indicates the server has stopped, and then saving the property file.
+		// Should all properties not defined in the llssite.cfg.in template be removed below?
+		String urlPropertyName = isIndependentGSI() ? GSI_URL : URL;
+		remove(urlPropertyName);
+		
+		remove("gsdlhome");
+		remove("collecthome");
+		remove("gdbmhome");
+		remove("logfilename");
+		
+		// For some reason launching GLI with server.exe clobbers autoenter and startbrowser 
+		// values for independent GSI in llssite.cfg file, and gli.autoenter and gli.startbrowser.
+		// So set them here.
+		if(get(GSI_AUTOENTER) == null) {
+			put(GSI_AUTOENTER, "0");
+		}
+		if(get(GSI_STARTBROWSER) == null) {
+			put(GSI_STARTBROWSER, "1");
+		}
+		if(get(AUTOENTER) == null) {
+			put(AUTOENTER, "1");
+		}
+		if(get(STARTBROWSER) == null) {
+			put(STARTBROWSER, "0");
+		}		
+		save();
+	    }
+	    else {
+		String urlPropertyName = isIndependentGSI() ? GSI_URL : URL;
+		debug("Restore Initial Settings");
+		put(AUTOENTER, autoenter_initial);
+		put(STARTBROWSER, start_browser_initial);
+		remove(urlPropertyName);
+		save();
+	    }
+	}
+
+	public void set() {
+	    debug("Set Session Settings");
+	    if(autoenter_initial == null) {
+		autoenter_initial = (String) get(AUTOENTER);
+		debug("Remember autoenter was: " + autoenter_initial);
+	    }
+	    put(AUTOENTER, TRUE);
+	    if(start_browser_initial == null) {
+		start_browser_initial = (String) get(STARTBROWSER);
+		debug("Remember start_browser was: " + start_browser_initial);
+	    }
+	    put(STARTBROWSER, FALSE);
+	    save();
+	}
+
+	private void debug(String message) {
+	    ///ystem.err.println(message);
+	}
+
+	private void save() {
+	    debug("Save: " + configFile.getAbsolutePath());
+	    try {
+		BufferedWriter out = new BufferedWriter(new FileWriter(configFile, false));
+		for(Iterator keys = keySet().iterator(); keys.hasNext(); ) {
+		    String key = (String) keys.next();
+		    String value = (String) get(key);
+		    out.write(key, 0, key.length());
+		    if(value != null) {
+			out.write('=');
+			
+			// if the GSI was launched outside of GLI, don't overwrite its default
+			// autoenter and startbrowser values
+			if(isIndependentGSI && (key == GSI_AUTOENTER || key == GSI_STARTBROWSER)) {
+			    if(key == GSI_AUTOENTER) {
+				out.write(autoenter_initial, 0, autoenter_initial.length());
+			    } else { // GSI_STARTBROWSER
+				out.write(start_browser_initial, 0, start_browser_initial.length());
+			    }
+			 } else {
+			     out.write(value, 0, value.length());
+			}
+		    }
+		    out.newLine();
+		}
+		out.flush();
+		out.close();
+	    }
+	    catch (Exception error) {
+		error.printStackTrace();
+	    }
+	}
+
+	private static void copyConfigFile(File source_cfg, File dest_cfg, boolean setToGliSiteDefaults) {
+	    // source_cfg file should exist
+	    // dest_cfg file should not yet exist
+	    // If setToGliSiteDefaults is true, then GLIsite.cfg's default configuration 
+	    // is applied to concerned lines: gli.autoenter=1, and gli.startbrowser=0
+
+	    try {
+		BufferedReader in = new BufferedReader(new FileReader(source_cfg));
+		BufferedWriter out = new BufferedWriter(new FileWriter(dest_cfg, false));
+		
+		String line = null;
+		while((line = in.readLine()) != null) {
+		    
+		    if(setToGliSiteDefaults) {
+			if(line.startsWith(AUTOENTER)) {
+			    line = AUTOENTER+"=1";
+			}
+			else if(line.startsWith(STARTBROWSER)) {
+			    line = STARTBROWSER+"=0";
+			}
+		    }
+		
+		    // write out the line
+		    out.write(line);
+		    out.newLine();
+		}
+		
+		out.flush();
+		in.close();
+		out.close();
+	    } catch(Exception e) {
+		System.err.println("Exception occurred when trying to copy the config file "
+				   + source_cfg.getName() + " to " + dest_cfg.getName() + ": " + e);
+		e.printStackTrace();
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/Plugins.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/Plugins.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone/Plugins.java	(revision 31635)
@@ -0,0 +1,338 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.greenstone;
+
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.Argument;
+import org.greenstone.gatherer.cdm.Plugin;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+import org.xml.sax.*;
+
+
+/** This class is for maintaining a list of known plug-ins, and importing new plugins using the parser. */
+public class Plugins
+{
+    // A list of all the plugins in the core Greenstone "perllib/plugins" folder (arguments may not be loaded)
+    static private ArrayList core_greenstone_plugins_list = null;
+    // The name of the loaded collection
+    static private String collection_name = null;
+    // A list of all the plugins in the loaded collection's "perllib/plugins" folder (arguments may not be loaded)
+    static private ArrayList collection_specific_plugins_list = new ArrayList();
+
+
+    static public Plugin getPlugin(String plugin_name, boolean arguments_required)
+    {
+	Plugin plugin = null;
+	boolean collection_specific = false;
+
+	// Check the collection-specific plugins first
+	for (int i = 0; i < collection_specific_plugins_list.size(); i++) {
+	    Plugin collection_specific_plugin = (Plugin) collection_specific_plugins_list.get(i);
+	    if (collection_specific_plugin.getName().equals(plugin_name)) {
+		plugin = collection_specific_plugin;
+		collection_specific = true;
+		break;
+	    }
+	}
+
+	// Try the core Greenstone plugins if necessary
+	if (plugin == null) {
+	    for (int i = 0; i < core_greenstone_plugins_list.size(); i++) {
+		Plugin core_greenstone_plugin = (Plugin) core_greenstone_plugins_list.get(i);
+		if (core_greenstone_plugin.getName().equals(plugin_name)) {
+		    plugin = core_greenstone_plugin;
+		    break;
+		}
+	    }
+	}
+
+	// If we've found the plugin, load its arguments now, if required
+	if (plugin != null && arguments_required) {
+	    if (!plugin.hasLoadedOptions()) {
+		loadPluginInfo(plugin, collection_specific);
+	    }
+	    else {
+		DebugStream.println("Already loaded arguments for " + plugin_name + "!");
+	    }
+	}
+
+	return plugin;
+    }
+
+
+    /** Returns a new list from merging the collection-specific and the core Greenstone plugins. */
+    static public ArrayList getPluginsList()
+    {
+	ArrayList plugins_list = new ArrayList();
+	plugins_list.addAll(collection_specific_plugins_list);
+
+	// Add in the core Greenstone plugins, taking care not to overwrite any collection-specific ones
+	for (int i = 0; i < core_greenstone_plugins_list.size(); i++) {
+	    Plugin core_greenstone_plugin = (Plugin) core_greenstone_plugins_list.get(i);
+
+	    boolean found = false;
+	    for (int j = 0; j < collection_specific_plugins_list.size(); j++) {
+		Plugin collection_specific_plugin = (Plugin) collection_specific_plugins_list.get(j);
+		if (core_greenstone_plugin.getName().equals(collection_specific_plugin.getName())) {
+		    found = true;
+		    break;
+		}
+	    }
+
+	    if (!found) {
+		plugins_list.add(core_greenstone_plugin);
+	    }
+	}
+
+	return plugins_list;
+    }
+
+
+    static private void loadPluginInfo(Plugin plugin, boolean collection_specific)
+    {
+	DebugStream.println("Loading arguments for " + plugin.getName() + "...");
+
+	// Run pluginfo.pl to get the list of plugins
+	try {
+	    String pluginfo_xml = null;
+	    if (Gatherer.isGsdlRemote) {
+		String pluginfo_options = "&plugin=" + plugin;
+		if (collection_specific) {
+		    pluginfo_options += "&collection=" + collection_name;
+		}
+		pluginfo_xml = Gatherer.remoteGreenstoneServer.getScriptOptions("pluginfo.pl", pluginfo_options);
+	    }
+	    else {
+		ArrayList args = new ArrayList();
+		args.add(Configuration.perl_path);
+		args.add("-S");
+		args.add(LocalGreenstone.getBinScriptDirectoryPath() + "pluginfo.pl");
+		args.add("-gs_version");
+		if (Gatherer.GS3) {
+		    args.add("3");
+		} else {
+		    args.add("2");
+		}
+		if (collection_specific) {
+		    args.add("-collection");
+		    args.add(collection_name);
+		    if (Gatherer.GS3) {
+		      args.add("-site");
+		      args.add(Configuration.site_name);
+		    }		    
+		}
+		args.add("-xml");
+		args.add("-language");
+		args.add(Configuration.getLanguage());
+		args.add(plugin.getName());
+		// Run the pluginfo.pl process
+		Runtime runtime = Runtime.getRuntime();
+		Process process = runtime.exec((String[]) args.toArray(new String[] { }));
+		InputStream input_stream = process.getErrorStream();
+		StringBuffer pluginfo_xml_buffer = XMLTools.readXMLStream(input_stream);
+		if (pluginfo_xml_buffer != null) {
+		    pluginfo_xml = pluginfo_xml_buffer.toString();
+		}
+	    }
+
+	    // Check the XML output was obtained successfully
+	    if (pluginfo_xml == null || pluginfo_xml.length() == 0) {
+		plugin.setHasLoadedOptions(false); // failure to load options
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.PlugInManager.PlugIn_XML_Parse_Failed", plugin.getName()), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		return;
+	    } else {
+		plugin.setHasLoadedOptions(true);
+	    }
+
+	    parsePluginInfoXML(plugin, pluginfo_xml);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+
+
+    static public void loadPluginsList(String collection_name_arg)
+    {
+	DebugStream.println("In loadPluginsList()...");
+
+	// If we're getting the collection-specific plugins, clear the old list no matter what
+	if (collection_name_arg != null) {
+	    collection_name = collection_name_arg;
+	    collection_specific_plugins_list = new ArrayList();
+	}
+
+	// Run pluginfo.pl to get the list of plugins
+	try {
+	    StringBuffer xml = null;
+	    if (Gatherer.isGsdlRemote) {
+		String pluginfo_options = "&listall";
+		if (collection_name != null) {
+		    pluginfo_options += "&collection=" + collection_name;
+		}
+		String pluginfo_output = Gatherer.remoteGreenstoneServer.getScriptOptions("pluginfo.pl", pluginfo_options);
+		xml = new StringBuffer(pluginfo_output);
+	    }
+	    else {
+		ArrayList args = new ArrayList();
+		args.add(Configuration.perl_path);
+		args.add("-S");		
+		args.add(LocalGreenstone.getBinScriptDirectoryPath() + "pluginfo.pl");
+		args.add("-gs_version");
+		if (Gatherer.GS3) {
+		    args.add("3");
+		} else {
+		    args.add("2");
+		}
+		if (collection_name != null) {
+		    args.add("-collection");
+		    args.add(collection_name);
+		    if (Gatherer.GS3) {
+		      args.add("-site");
+		      args.add(Configuration.site_name);
+		    }
+		}
+		args.add("-listall");
+		args.add("-xml");
+
+		// Run the pluginfo.pl process
+		Runtime runtime = Runtime.getRuntime();
+		Process process = runtime.exec((String[]) args.toArray(new String[] { }));
+		InputStream input_stream = process.getErrorStream();
+		xml = XMLTools.readXMLStream(input_stream);
+	    }
+
+	    // Check the XML output was obtained successfully
+	    if (xml == null || xml.length() == 0) {
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.PluginManager.Plugin_List_XML_Parse_Failed"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		return;
+	    }
+
+	    if (collection_name != null) {
+		collection_specific_plugins_list = parsePluginsListXML(xml.toString());
+	    }
+	    else {
+		core_greenstone_plugins_list = parsePluginsListXML(xml.toString());
+	    }
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+
+
+    static private void parsePluginInfoXML(Plugin plugin, String xml)
+    {
+	Document document = XMLTools.parseXML(new StringReader(xml));
+	if (document == null) {
+	    plugin.setHasLoadedOptions(false); // failure to load the options/failed plugin
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.PlugInManager.PlugIn_XML_Parse_Failed", plugin.getName()), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	    return;
+	} else {
+	    plugin.setHasLoadedOptions(true);
+	}
+
+	parsePluginInfoXMLNode(plugin, document.getDocumentElement());
+    }
+
+
+    static private void parsePluginInfoXMLNode(Plugin plugin, Node root_node)
+    {
+	for (Node node = root_node.getFirstChild(); node != null; node = node.getNextSibling()) {
+	    String node_name = node.getNodeName();
+
+	    if (node_name.equalsIgnoreCase("Name")) {
+		plugin.setName(XMLTools.getValue(node));
+	    }
+	    else if (node_name.equals("Desc")) {
+		plugin.setDescription(XMLTools.getValue(node));
+	    }
+	    else if (node_name.equals("Abstract")) {
+		plugin.setIsAbstract(XMLTools.getValue(node).equalsIgnoreCase(StaticStrings.YES_STR));
+	    }
+	    else if (node_name.equalsIgnoreCase("Explodes")) {
+		plugin.setDoesExplodeMetadataDatabases(XMLTools.getValue(node).equalsIgnoreCase(StaticStrings.YES_STR));
+	    }
+	    else if (node_name.equalsIgnoreCase("SourceReplaceable")) { // looking for <SourceReplaceable> tag
+		plugin.setDoesReplaceSrcDocsWithHtml(XMLTools.getValue(node).equalsIgnoreCase(StaticStrings.YES_STR));
+	    }
+	    else if (node_name.equalsIgnoreCase("Processes")) {
+		plugin.setDefaultProcessExpression(XMLTools.getValue(node));
+	    }
+	    else if (node_name.equalsIgnoreCase("Blocks")) {
+		plugin.setDefaultBlockExpression(XMLTools.getValue(node));
+	    }
+	    // Parse the plugin arguments
+	    else if (node_name.equalsIgnoreCase("Arguments")) {
+		for (Node argument_node = node.getFirstChild(); argument_node != null; argument_node = argument_node.getNextSibling()) {
+		    // An option
+		    if (argument_node.getNodeName().equalsIgnoreCase("Option")) {
+			Argument argument = new Argument();
+			argument.parseXML((Element) argument_node);
+			plugin.addArgument(argument);
+		    }
+		}
+	    }
+	    // A super plugin class
+	    else if (node_name.equalsIgnoreCase("PlugInfo")) {
+		Plugin super_plugin = new Plugin();
+		parsePluginInfoXMLNode(super_plugin, node);
+		plugin.setSuper(super_plugin);
+	    }
+	}
+    }
+
+
+    static private ArrayList parsePluginsListXML(String xml)
+    {
+	ArrayList plugins_list = new ArrayList();
+	
+	Document document = XMLTools.parseXML(new StringReader(xml));
+	Node root = document.getDocumentElement();
+	for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) {
+	    String node_name = node.getNodeName();
+
+	    if (node_name.equals("PlugInfo")) {
+		Plugin plugin = new Plugin();
+		parsePluginInfoXMLNode(plugin, node);
+		plugins_list.add(plugin);
+	    }
+	}
+
+	return plugins_list;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone3/ServletConfiguration.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone3/ServletConfiguration.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/greenstone3/ServletConfiguration.java	(revision 31635)
@@ -0,0 +1,189 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.greenstone3;
+
+import java.awt.*;
+import java.io.*;
+import java.lang.ref.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.plaf.*;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.util.ArrayTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.w3c.dom.*;
+
+/** This class stores the mapping between sites and servlets available for them. The info is read in from the web.xml file.
+ * @author Katherine Don, New Zealand Digital Library, University of Waikato
+ * @version 
+ */
+public class ServletConfiguration {
+    
+    static public String ADD_COMMAND = "?a=s&sa=a&st=collection&sn=";
+    static public String DEACTIVATE_COMMAND = "?a=s&sa=d&st=collection&sn=";
+    private String gsdl3_path = "";
+    private ArrayList sites = null;
+    private HashMap mappings = null;
+
+    public ServletConfiguration(String gsdl3_path) {
+	
+	this.gsdl3_path = gsdl3_path;
+
+	if (Gatherer.isGsdlRemote){
+	    if (Gatherer.remoteGreenstoneServer.downloadWebXMLFile().equals("")) {
+		System.err.println("Error: Could not download web.xml.");
+		System.exit(0);
+	    }
+	}
+	//String web_xml_path = gsdl3_path + File.separator + "web" + File.separator + "WEB-INF"+ File.separator + "web.xml";
+	File web_xml;
+	if (Gatherer.isGsdlRemote){
+	    web_xml = new File(gsdl3_path + "web.xml");
+	}else{
+	    web_xml = new File(gsdl3_path + "WEB-INF"+ File.separator + "web.xml");
+	}
+	if (!web_xml.exists()) {
+	    DebugStream.println("Error: no web.xml found at "+web_xml.toString());
+	    return;
+	}
+
+	this.sites = new ArrayList();
+	if (Gatherer.isGsdlRemote){
+	    String sites_on_server = Gatherer.remoteGreenstoneServer.getSiteNames();
+	    if (sites_on_server.equals("")) {
+		// !! Something went wrong : could not get names of the sites
+		System.err.println("Error: Could not get names of the sites.");
+		System.exit(0);
+	    }
+	   
+	    String[] sites_arr=sites_on_server.split("-----");
+	    for (int i=0; i<sites_arr.length; i++){
+		if (!(sites_arr[i].trim().equals(""))){
+		    this.sites.add(sites_arr[i].trim());
+		}
+	    }
+	}else{
+	    // find the sites
+	    File start = new File(Utility.getSitesDir(gsdl3_path));
+	    File possible_sites[] = start.listFiles();
+	    ArrayTools.sort(possible_sites);
+	    for (int i=0; possible_sites != null && i < possible_sites.length; i++) {
+		File site_config = new File(possible_sites[i], "siteConfig.xml");
+		if (site_config.exists()) {
+		    this.sites.add(possible_sites[i].getName());
+		}
+	    }
+	}
+
+	this.mappings = new HashMap();
+	Document web_config = XMLTools.parseXMLFile(web_xml.getAbsolutePath(), false);
+       
+	NodeList servlet_mappings = web_config.getElementsByTagName("servlet-mapping");
+	// make a little map class
+	HashMap url_mappings = new HashMap();
+	for (int i=0; i<servlet_mappings.getLength(); i++) {
+	    Element map = (Element)servlet_mappings.item(i);
+	    String name = XMLTools.getValue(XMLTools.getNodeFromNamed(map, "servlet-name"));
+	    String pattern = XMLTools.getValue(XMLTools.getNodeFromNamed(map, "url-pattern"));
+	    url_mappings.put(name, pattern);
+
+	}
+
+	NodeList servlets = web_config.getElementsByTagName("servlet");
+	for (int i=0; i<servlets.getLength(); i++) {
+	    Element servlet = (Element)servlets.item(i);
+	    String name = XMLTools.getValue(XMLTools.getNodeFromNamed(servlet, "servlet-name"));
+	    String description = XMLTools.getValue(XMLTools.getNodeFromNamed(servlet, "description"));
+	    String site = getServletSite(servlet);
+	       
+	    if (site != null) {
+		ArrayList this_site = (ArrayList)mappings.get(site);
+		if (this_site == null) {
+		    this_site = new ArrayList();
+		}
+		String url = (String)url_mappings.get(name);
+		if(url.endsWith("/*")) { // truncate trailing /*, e.g. /library/* becomes /library
+		    url = url.substring(0, url.length()-2);
+		}
+		this_site.add(url);
+		mappings.put(site, this_site);
+	    }
+	    
+	}
+
+	web_config = null;
+	url_mappings = null;
+    }
+    
+    public ArrayList getSites() {
+	return this.sites;
+    }
+
+    public ArrayList getServletsForSite(String site) {
+	return (ArrayList)this.mappings.get(site);
+    }
+    public String getServletPath(String site) {
+	
+	// for now, get the first one
+	ArrayList servlets = (ArrayList)mappings.get(site);
+	if(servlets == null) {
+	    return null;
+	}
+	return (String)servlets.get(0);
+    }
+
+    private String getServletSite(Element servlet) {
+	
+	NodeList params = servlet.getElementsByTagName("init-param");
+	if ( params == null || params.getLength() == 0) {
+	    return null;
+	}
+	String site = null;
+	for (int i=0; i<params.getLength()&& site == null; i++) {
+	    String p_name = XMLTools.getValue(XMLTools.getNodeFromNamed(params.item(i), "param-name"));
+	    if (p_name.equals("site_name")) {
+		site = XMLTools.getValue(XMLTools.getNodeFromNamed(params.item(i), "param-value"));
+	    }
+	}
+	return site;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/AboutDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/AboutDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/AboutDialog.java	(revision 31635)
@@ -0,0 +1,186 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Greenstone Librarian Interface (GLI) application, 
+ * part of the Greenstone digital library software suite from the New 
+ * Zealand Digital Library Project at the University of Waikato, 
+ * New Zealand.
+ *
+ * Author: John Thompson
+ *         Greenstone Project, New Zealand Digital Library
+ *         University of Waikato
+ *         http://www.nzdl.org
+ *
+ * Copyright (C) 2004 New Zealand Digital Library, University of Waikato
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.Utility;
+
+/** Generates a pretty about dialog which not only thanks those for their contributions but also meets our legal requirements if we wish to ship the JVM with GLI.
+ * @author John Thompson, Greenstone Project, New Zealand Digital Library, University of Waikato
+ * @version 2.41 final
+ */
+// RICOH SOURCE CODE PUBLIC LICENSE - http://www.risource.org/RPL/RPL-1.0A.shtml
+public class AboutDialog
+    extends JDialog {
+    /** The default size of the about dialog. */
+    static final private Dimension SIZE = new Dimension(600, 325);
+    /** The size of the GLI icon to display on dialog. */
+    static final private int ICON_SIZE = 65;
+    /** A reference to ourself so that our inner classes can dismiss us. */
+    private AboutDialog self;
+    /** The button used for dismissing the about dialog. */
+    private JButton close_button;
+    /** The constructor not only builds, but displays the about dialog. This method doesn't return until the dialog is dismissed.
+     * @param parent the JFrame which owns this dialog for use in centering the dialog
+     * @see org.greenstone.gatherer.Dictionary#get
+     * @see org.greenstone.gatherer.Dictionary#setBoth
+     * @see org.greenstone.gatherer.Dictionary#setText
+     * @see org.greenstone.gatherer.gui.AboutDialog.CloseButtonListener
+     * @see org.greenstone.gatherer.gui.GLIButton
+     */
+    public AboutDialog(JFrame parent) {
+	super(parent, Dictionary.get("AboutDialog.Title"), true);
+	this.self = this;
+	setSize(SIZE);
+        this.setComponentOrientation(Dictionary.getOrientation());        
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel upper_pane = new JPanel();
+        upper_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	String gmedium_image = "gatherer_medium.png";
+	if (Configuration.fedora_info.isActive()) {
+	    gmedium_image = "fli-" + gmedium_image;
+	}
+
+	ImageIcon icon = JarTools.getImage(gmedium_image);
+	ImageIcon scaled_icon = new ImageIcon(icon.getImage().getScaledInstance(ICON_SIZE, ICON_SIZE, Image.SCALE_DEFAULT));
+	JLabel icon_label = new JLabel(scaled_icon);
+	icon_label.setComponentOrientation(Dictionary.getOrientation());
+        
+        JPanel title_pane = new JPanel();
+	title_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+        JLabel title_one_label = new JLabel(Dictionary.get("AboutDialog.Title_One"));
+	title_one_label.setComponentOrientation(Dictionary.getOrientation());
+        JLabel title_two_label = new JLabel(Gatherer.PROGRAM_NAME + " " + Gatherer.PROGRAM_VERSION + " " + Dictionary.get("AboutDialog.Date"));
+        title_two_label.setComponentOrientation(Dictionary.getOrientation());
+        JLabel title_three_label = new JLabel(Dictionary.get("AboutDialog.Title_Two"));
+	title_three_label.setComponentOrientation(Dictionary.getOrientation());
+        JLabel copyright_label = new JLabel(Dictionary.get("AboutDialog.Copyright"));
+	copyright_label.setComponentOrientation(Dictionary.getOrientation());
+        JLabel gpl_label = new JLabel(Dictionary.get("AboutDialog.Copyright_Two"));
+        gpl_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	JTextArea text = new JTextArea();
+        text.setComponentOrientation(Dictionary.getOrientation());
+	text.setLineWrap(true);
+	text.setWrapStyleWord(true);
+        
+        
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	close_button = new GLIButton(Dictionary.get("General.Close"), Dictionary.get("General.Close_Tooltip"));
+	
+	// Connection
+	close_button.addActionListener(new CloseButtonListener());
+
+	// Layout
+	icon_label.setBorder(BorderFactory.createEmptyBorder(0,0,0,10));
+
+	title_pane.setLayout(new GridLayout(5,1,0,2));
+	title_pane.add(title_one_label);
+	title_pane.add(title_two_label);
+	title_pane.add(title_three_label);
+	title_pane.add(copyright_label);
+	title_pane.add(gpl_label);
+		  
+	upper_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+	upper_pane.setLayout(new BorderLayout());
+	upper_pane.add(icon_label, BorderLayout.LINE_START);
+	upper_pane.add(title_pane, BorderLayout.CENTER);
+
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	button_pane.setLayout(new BorderLayout());
+	button_pane.add(close_button, BorderLayout.LINE_END);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(upper_pane, BorderLayout.NORTH);
+	content_pane.add(new JScrollPane(text), BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	// Build text content
+	text.append(Dictionary.get("AboutDialog.Java_Req"));
+	text.append("\n");
+	text.append(Dictionary.get("AboutDialog.Java_Req_One"));
+	text.append("\n");
+	text.append(Dictionary.get("AboutDialog.Java_Req_Two"));
+	text.append("\n\n");
+	text.append("*****" + Dictionary.get("AboutDialog.Acknowledgement") + "*****");
+	text.append("\n\n");
+	text.append(Dictionary.get("AboutDialog.Item0"));
+	text.append("\n\n");
+	text.append(Dictionary.get("AboutDialog.Item2"));
+	text.append("\n\n");
+	text.append(Dictionary.get("AboutDialog.Item3"));
+	text.append("\n\n");
+	text.append("*****" + Dictionary.get("AboutDialog.Thanks") + "*****");
+	text.append("\n\n");
+	text.append(Dictionary.get("AboutDialog.Item4"));
+	text.append("\n\n");
+	text.append(Dictionary.get("AboutDialog.Item5"));
+	text.append("\n\n");
+	text.append(Dictionary.get("AboutDialog.Item6"));
+	text.append("\n\n");
+	text.append(Dictionary.get("AboutDialog.Item7"));
+	text.append("\n\n");
+	text.append(Dictionary.get("AboutDialog.Item8"));
+	text.append("\n\n");
+	text.append(Dictionary.get("AboutDialog.Item9"));
+	text.append("\n\n");
+	text.setCaretPosition(0);
+
+	// Show
+	Rectangle frame_bounds = parent.getBounds();
+	setLocation(frame_bounds.x + (frame_bounds.width - SIZE.width) / 2, frame_bounds.y + (frame_bounds.height - SIZE.height) / 2);
+	setVisible(true);
+    }
+    /** Listens for actions upon the close button, and when detected closes the dialog. */
+    private class CloseButtonListener
+	implements ActionListener {
+	/** Called whenever an action occurs on the close button, thus asking the dialog to close.
+	 * @param event an ActionEvent containing information about the button press
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    self.setVisible(false);
+	    self.dispose();
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/BaseConfigPane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/BaseConfigPane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/BaseConfigPane.java	(revision 31635)
@@ -0,0 +1,252 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.cdm.Control;
+
+/** This class is responsible for generating the necessary GUI components. It does this by calling the getEditControls() method of the appropriate class (i.e. IndexManager for index related edits). It also is in charge of correctly adding (and removing) listeners, using phrases from the <strong>Dictionary</strong> rather than the text keys inserted in the component classes, and several other aspects of the design page, including the config file section tree.
+
+/** This is a base class for Design and Format panes, which are very similar except that they have different components.  */
+public abstract class BaseConfigPane 
+    extends JPanel {
+
+    /* This should list the contents of the contents */
+    protected String contents[]; 
+    
+    /** The preferred size of the contents tree. */
+    static final private Dimension TREE_SIZE = new Dimension(200, 500);
+
+    /** The panel apon which is rendered the currently selected section screen. */
+    protected Control view = null;
+    protected String view_type = null;
+    /** The collection manager is responsible for parsing in, and allowing the editing of, a single collections configuration file. */
+    protected CollectionDesignManager cdm = null;
+    /** A tree to serve as a 'table of contents' for this design tool. We decided on a tree rather than a list, as it allows us to break sections into subsections if they become to complicated. */
+    protected DesignTree tree;
+    protected JPanel tree_pane;
+    /** The constructor. */
+    public BaseConfigPane() {
+	super();
+	DebugStream.println("BaseConfigPane: Main GUI components created.");
+	// Creation
+        this.setComponentOrientation(Dictionary.getOrientation());
+	tree_pane = new JPanel();
+        tree_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	tree = new DesignTree();
+	tree.setComponentOrientation(Dictionary.getOrientation());
+	// Connect
+	tree.addTreeSelectionListener(new TreeListener());
+
+	// Layout
+	tree_pane.setLayout(new BorderLayout());
+	tree_pane.setPreferredSize(TREE_SIZE);
+        JScrollPane scrol_tmp;
+        scrol_tmp = new JScrollPane(tree);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	tree_pane.add(scrol_tmp, BorderLayout.CENTER);
+	setLayout(new BorderLayout());
+	setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	
+	add(tree_pane, BorderLayout.LINE_START);
+
+
+    }
+
+    abstract protected Control getSubControls(String type);
+    
+    /** Called to cause the components to lay themselves out and be displayed.
+     */
+    public void display() {
+    }
+
+
+    public void gainFocus()
+    {
+	if (cdm == null && Gatherer.c_man.ready()) {
+	    // Retrieve the new config manager.
+	    cdm = Gatherer.c_man.getCollection().cdm;
+	}
+	if (view != null) {
+	    view.gainFocus();
+	}
+    }
+
+
+    public void loseFocus()
+    {
+	if (view != null) {
+	    view.loseFocus();
+	}
+	if (cdm != null) {
+	    Gatherer.c_man.saveCollection();
+	}
+    }
+
+
+    public void destroy() {
+	tree = null;
+	view = null;
+    }
+
+    /** Called whenever the detail mode changes to ensure the filters are at an appropriate level (ie only editable by those that understand regular expression matching)
+     * @param mode the mode level as an int
+     */
+    public void modeChanged(int mode) {
+	if (cdm != null) {
+	    cdm.modeChanged(mode);
+	}
+    }
+
+    /** This method is called whenever the state of the current collection changes.
+     */
+    public void refresh(int refresh_reason, boolean ready)
+    {
+	if (ready) {
+	    // Retrieve the new config manager.
+	    cdm = Gatherer.c_man.getCollection().cdm;
+	    tree.resetModel(Configuration.getMode()); // does this set view??
+	}
+    }
+
+
+    /** Force the display to show a certain pane of controls.
+     * @param type a String giving the name of the information manager or view we wish to display
+     */
+    public void setSelectedView(String type) {
+	tree.setSelectedView(type);
+    }
+
+    /** This tree provides a 'table of contents' for the various components of the design process (collection configuration in more technical terms). */
+    private class DesignTree
+	extends JTree {
+	private DesignNode root = null;
+	/** Constructor. Automatically generates all of the nodes, in the order of contents. */
+	public DesignTree() {
+	    super();
+	}
+
+	/** Reset the model used by the design page contents tree. This is necessary to hide the partitions entry when in lower detail modes
+	 * @param mode the current detail mode as an int
+	 */
+	public void resetModel(int mode) {
+	    root = new DesignNode("CDM.GUI.Root");
+	    // Now add the design categories.
+	    for(int i = 0; i < contents.length; i++) {
+		root.add(new DesignNode(contents[i]));
+	    }
+	    this.setModel(new DefaultTreeModel(root));
+	    expandRow(0);
+	    setRootVisible(false);
+	    setSelectionRow(0);
+	    updateUI();
+	}
+	/** Set the current view to the one specified.
+	 * @param type the name of the desired view as a String
+	 */
+	public void setSelectedView(String type) {
+	    type = Dictionary.get(type);
+	    for(int i = 0; i < root.getChildCount(); i++) {
+		DesignNode child = (DesignNode) root.getChildAt(i);
+		if(child.toString().equals(type)) {
+		    TreePath path = new TreePath(child.getPath());
+		    setSelectionPath(path);
+		}
+	    }
+	}
+    }
+    /** A tree node that retains a reference to one of the possible design sub-views relating to the different sub-managers. */
+    private class DesignNode
+	extends DefaultMutableTreeNode {
+	/** Constructor.
+	 * @param object The <strong>Object</strong> assigned to this node.
+	 */
+	public DesignNode(String object) {
+	    super(object);
+	}
+	/** Retrieve a textual representation of the object.
+	 * @return a String
+	 */
+	public String toString() {
+	    // return Dictionary.get("CDM.GUI." + (String)getUserObject());
+	    return Dictionary.get((String) getUserObject());
+	}
+    }
+    /** Listens for selection changes in the 'contents' tree, and switches to the appropriate view. */
+    private class TreeListener
+	implements TreeSelectionListener {
+	/** Called whenever the selection changes, we must update the view so it matches the node selected.
+	 * @param event A <strong>TreeSelectionEvent</strong> containing more information about the tree selection.
+	 */
+	public void valueChanged(TreeSelectionEvent event) {
+	    if(!tree.isSelectionEmpty()) {
+		TreePath path = tree.getSelectionPath();
+		DesignNode node = (DesignNode)path.getLastPathComponent();
+		String type = (String)node.getUserObject();
+		// Wait begins
+		Gatherer.g_man.wait(true);
+		// Save information in current view
+		if (view != null) { // can we get rid of this??
+		    view.loseFocus();
+		    remove((JPanel)view);
+		}
+		view_type = type;
+		// Change panes.
+		view = getSubControls(type);
+		if (view != null) {
+		    add((JPanel)view, BorderLayout.CENTER);
+		    // Update information on visible pane
+		    view.gainFocus();
+		}
+		repaint();
+		// Wait ends
+		Gatherer.g_man.wait(false);
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ConfigFileEditor.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ConfigFileEditor.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ConfigFileEditor.java	(revision 31635)
@@ -0,0 +1,271 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.gui;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+
+import org.w3c.dom.*;
+
+
+import java.io.File;
+import java.io.IOException;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import javax.swing.event.*;
+
+
+public class ConfigFileEditor extends ModalDialog 
+    implements ActionListener, DocumentListener
+{
+    public final File config_file;
+
+    private GLIButton cancel_button = null;
+    private GLIButton save_button = null;
+    private NumberedJTextArea editor = null;
+    private JTextArea editor_msgarea;
+
+    //private final String DEFAULT_PROCESSING_INSTRUCTION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";    
+
+    private static final Dimension SIZE = new Dimension(850,550);
+
+    public ConfigFileEditor() {
+
+      super(Gatherer.g_man, true);
+      setModal(true);
+      setSize(SIZE);
+
+	String collection_folder_path = Gatherer.getCollectDirectoryPath();
+	String collectionName  = Gatherer.c_man.getCollection().getName(); // we won't be in this constructor if collection is null.
+	this.config_file = new File(collection_folder_path + File.separator + collectionName, Utility.CONFIG_GS3_FILE); // col config editing is a GS3 feature
+
+	
+	// Most of the validation_msg_panel code is from Format4gs3Manager.java
+	JPanel validation_msg_panel = new JPanel();
+	JLabel validation_label = new JLabel(Dictionary.get("CDM.FormatManager.MessageBox"));
+	validation_label.setBorder(new EmptyBorder(10,10,10,10)); // add some margin
+	editor_msgarea = new JTextArea();
+	
+	editor_msgarea.setCaretPosition(0);
+	editor_msgarea.setLineWrap(true);
+	editor_msgarea.setRows(3);
+	editor_msgarea.setWrapStyleWord(false);
+	editor_msgarea.setEditable(false);
+	editor_msgarea.setToolTipText(Dictionary.get("CDM.FormatManager.MessageBox_Tooltip"));
+	validation_msg_panel.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
+	validation_msg_panel.setLayout(new BorderLayout(5, 0));
+	validation_msg_panel.add(validation_label, BorderLayout.WEST);
+	validation_msg_panel.add(new JScrollPane(editor_msgarea), BorderLayout.CENTER);
+	
+	// the all important XML editor
+	editor = new NumberedJTextArea(Dictionary.get("ConfigFileEditor.Tooltip"));
+	editor.getDocument().addDocumentListener(this);
+
+	save_button = new GLIButton(Dictionary.get("General.Save"), Dictionary.get("ConfigFileEditor.Save_Tooltip"));
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("ConfigFileEditor.Cancel_Tooltip")); // tooltip is the same
+	cancel_button.addActionListener(this);
+	save_button.addActionListener(this);
+
+	// LAYOUT
+	JPanel button_panel = new JPanel(new FlowLayout());
+	button_panel.setComponentOrientation(Dictionary.getOrientation());
+	button_panel.add(editor.undoButton);
+	button_panel.add(editor.redoButton);
+	button_panel.add(cancel_button);
+	button_panel.add(save_button);
+
+	JPanel content_pane = (JPanel) getContentPane();
+	content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(validation_msg_panel, BorderLayout.NORTH);
+	content_pane.add(new JScrollPane(editor), BorderLayout.CENTER); // make editor scrollable
+	content_pane.add(button_panel, BorderLayout.SOUTH);
+
+	
+	// NOW WE CAN PREPARE THE ACTUAL CONTENT TO GO INTO THE XML EDITOR TEXTAREA
+	Document xmlDoc = null;
+	xmlDoc = XMLTools.parseXMLFile(config_file);
+
+	if(xmlDoc != null) { 
+
+	    // Save the collectionConfig.xml before loading it.
+	    String[] nonEscapingTagNames = { StaticStrings.FORMAT_STR, StaticStrings.DISPLAYITEM_STR };
+	    XMLTools.writeXMLFile(this.config_file, xmlDoc, nonEscapingTagNames); //prepends proc instruction
+
+	    // now load the contents
+	    editor_msgarea.setBackground(Color.white);
+	    StringBuffer sb = new StringBuffer();
+	    XMLTools.xmlNodeToString(sb, xmlDoc.getDocumentElement(), true, "  ", 0);
+	    editor.setText(sb.toString());
+	    //editor.setText(elementToString(xmlDoc.getDocumentElement(), true)); // reading in file this way adds processing instruction, which the parser doesn't like.
+
+	} else { // parsing failed
+	    // so read in file as text and display as text and show the validation box in red
+
+	    // Won't re-save file that is already invalid before it's even been edited through this Editor
+	    System.err.println("*** Warning: Parsing error in file " + config_file + ".");
+	    System.err.println("*** Not saving ahead of editing.");
+
+	    String xml_str = Utility.readFile(config_file);
+
+	    // re-parse to get error msg to display in validation field
+
+	    // but first get rid of the preprocessing instruction, as this will fail parsing
+	    String processingInstruction = getProcessingInstruction(xml_str);
+	    xml_str = xml_str.replace(processingInstruction, "").trim(); // also trim newline at start
+
+	    if(!xml_str.equals("")) {
+
+		String msg = XMLTools.parse(xml_str); // re-parse
+		editor_msgarea.setBackground(Color.red);
+		editor_msgarea.setText(msg); // display the parsing error
+		save_button.setEnabled(false);
+
+		editor.setText(xml_str); // display the xml contents with error and all		
+		
+	    } else {
+		editor.setText("");
+		editor_msgarea.setText("Empty collection config file");
+	    }	    
+	}
+
+	// Final dialog setup & positioning.
+	setDefaultCloseOperation(DISPOSE_ON_CLOSE); // get rid of this dialog when it's closed (on dispose())
+	getRootPane().setDefaultButton(save_button);
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+    }
+
+    public void actionPerformed(ActionEvent e) {
+	
+	if(e.getSource() == cancel_button) {
+	    this.dispose(); // get rid of this dialog, we're done
+	} 
+	else if(e.getSource() == save_button) {
+	    // write out the XML to the collectionConfig.xml file and we're done.
+
+	    // already know the xml in the textarea is valid, else the save_button would have been inactive
+	    Document xml_file_doc = XMLTools.getDOM(editor.getText());
+	    // This code from FormatConversionDialog.dispose() 
+	    // saves the XML, prepending the processing instruction (<?xml ... ?>)
+	    String[] nonEscapingTagNames = { StaticStrings.FORMAT_STR, StaticStrings.DISPLAYITEM_STR };
+	    XMLTools.writeXMLFile(this.config_file, xml_file_doc, nonEscapingTagNames);
+
+	    this.dispose(); // get rid of this dialog, we're done
+
+	    // close and reopen the collection in GLI
+	    String current_collection_filepath = Gatherer.c_man.getCollection().getCollectionPath();
+	    Gatherer.c_man.closeCollection();
+	    Gatherer.c_man.loadCollection(current_collection_filepath);
+	}
+    }
+
+    
+   
+    // This method returns a proper processing instruction (starts with <?xml and ends with ?>)
+    // else the empty string is returned
+    public String getProcessingInstruction(String xmlStr) {
+	String processingInstruction = "";
+
+	xmlStr = xmlStr.trim();
+	if(xmlStr.startsWith("<?xml")) {
+	    int endIndex = xmlStr.indexOf("?>"); // end of processing instruction
+	    
+	    if(endIndex != -1) {
+		endIndex += 2;
+		processingInstruction = xmlStr.substring(0, endIndex);
+	    }
+	}	
+	return processingInstruction;
+    }
+
+
+    // THE FOLLOWING FUNCTIONS ARE LARGELY FROM Format4gs3Manager.java.EditorListener
+    public void changedUpdate(DocumentEvent e)
+    {
+	update();
+    }
+    
+    public void insertUpdate(DocumentEvent e)
+    {
+	update();
+	updateUndo("insert");
+	
+    }
+    
+    public void removeUpdate(DocumentEvent e)
+    {
+	update();
+	updateUndo("remove");
+	
+    }
+    
+    private void updateUndo(String from)
+    {
+	
+	editor.undoButton.setEnabled(true);
+    }
+    
+    public void update()
+    {
+	
+	String xml_str = editor.getText();
+
+	// Processing instructions "<?xml...?>" are no longer displayed in the editor (but
+	// they are written out to the file on save) so we don't need to deal with them here.
+	
+	// can't parse with processing instruction <?xml...?>, so remove that
+	///String processingInstruction = getProcessingInstruction(xml_str);
+	///xml_str = xml_str.replace(processingInstruction, "");	
+	// now can parse the XML portion without the processing instruction,
+	// while the original XML remains unchanged in the editor area	
+	// If the processing instruction was incomplete, then xml_str would still
+	// include the incomplete processing instruction, and parsing below will catch the error.
+	
+	String msg = XMLTools.parse(xml_str);
+	editor_msgarea.setText(msg);
+	
+	if (msg.startsWith(XMLTools.WELLFORMED))
+	    {		   
+		editor_msgarea.setBackground(Color.white);
+		save_button.setEnabled(true);
+	    }
+	else
+	    {		 
+		editor_msgarea.setBackground(Color.red);
+		save_button.setEnabled(false);
+	    }
+    }
+    
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/CreatePane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/CreatePane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/CreatePane.java	(revision 31635)
@@ -0,0 +1,1097 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.BuildTypeManager;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.collection.Collection;
+import org.greenstone.gatherer.shell.GBuildProgressMonitor;
+import org.greenstone.gatherer.shell.GImportProgressMonitor;
+import org.greenstone.gatherer.shell.GScheduleProgressMonitor; 
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.shell.GShellEvent;
+import org.greenstone.gatherer.shell.GShellListener;
+import org.greenstone.gatherer.shell.GShellProgressMonitor;
+import org.greenstone.gatherer.util.AppendLineOnlyFileDocument;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.util.GS3ServerThread;
+
+/** This class provides a GUI view showing some statistics on your current collection, and options for building it. As the collection is built this initial view is replaced with one showing progress bars and a message log of the creation process. This log can be later accessed via the options tree located in the center of the initial pane. This class is also responsible for creating the GShellProgressMonitors that are then attatched to external shell processes, and calling the methods in the CollectionManager for actually importing and building the collection. <br><BR>
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class CreatePane
+    extends JPanel
+    implements GShellListener {
+
+    static private Dimension ARGUMENT_SIZE = new Dimension(800,90);
+    /** The threshold for when the simple view is replaced by the complex one. */
+    static private final int THRESHOLD = Configuration.EXPERT_MODE;
+    /** An identifier for the control panel within the card layout. */
+    static private String CONTROL  = "Control";
+    /** An identifier for the progress panel within the card layout. */
+    static private String PROGRESS = "Progress";
+    /** An identifier for the simple panel within the card layout. */
+    static private String SIMPLE   = "Simple";
+
+    /** Determines the current view that should be shown for this pane. */
+    public boolean processing = false;
+    /** The options pane generates all the various option sheet configuations. */
+    public OptionsPane options_pane = null;
+    private AppendLineOnlyFileDocument document;
+    /** A card layout is used to store the separate configuration and progress panes. */
+    private CardLayout card_layout = null;
+    /** Stores a reference to the current collection. */
+    private Collection previous_collection = null;
+    /** This monitor tracks the build processes progress. */
+    private GShellProgressMonitor build_monitor = null;
+    /** This monitor tracks the import processes progress. */
+    private GShellProgressMonitor import_monitor = null;
+    /** This monitor tracks the schedule processes progress. */ 
+    private GShellProgressMonitor schedule_monitor = null; 
+    /** The button for begining the building processes. */
+    private JButton build_button = null;
+    /** The button for stopping the building processes. */
+    private JButton cancel_button = null;
+    /** The button for setting up scheduling */
+    private JButton schedule_button = null; 
+    /** The button for viewing the collection. */
+    private JButton preview_button = null;
+    private JButton simple_build_button;
+    private JButton simple_cancel_button;
+    private JButton simple_preview_button;
+    /** The radio buttons for chnaging the build type (incremental/full) */
+    private JRadioButton full_build_radio_button;
+    private JRadioButton incremental_build_radio_button;
+    private JRadioButton sfull_build_radio_button;
+    private JRadioButton sincremental_build_radio_button;
+    /** The label displaying the number of documents in this collection. */
+    private JLabel document_count = null;
+    /** The label alongside the build progress bar gets some special treatment so... */
+    private JLabel progress_build_label = null;
+    /** The label alongside the import progress bar gets some special treatment so... */
+    private JLabel progress_import_label = null;
+    private JPanel bar_area;
+    /** The panel on which buttons are rendered on higher detail modes. */
+    private JPanel button_pane;
+    /** The pane which contains the controls for configuration. */
+    private JPanel control_pane = null;
+    /** The pane which has the card layout as its manager. */
+    private JPanel main_pane = null;
+    /** The pane which contains the progress information. */
+    private JPanel progress_pane = null;
+    /** The pane on the right-hand side - shows the requested options view */
+    private JPanel right = null;
+    /** The simple panel on which all of the available arguments are rendered. */
+    private JPanel sargument_configuration_panel;
+    /** The panel on which buttons are rendered on lower details modes. */
+    private JPanel sbutton_panel;
+    /** A simplified version for lower detail modes containing both the control and progress panes smooshed together. */
+    private JSplitPane simple_panel;
+    /** The lower part of the panel which is global so the log pane can be added and removed from it */
+    private JPanel slower_panel;
+    /** The inner panel of the simple pane which is global so that the bar_area can be added and removed from it. */
+    private JPanel sinner_panel;
+    /** The scrolling pane for the log. */
+    private JScrollPane log_scroll;
+    /** The scrolling pane the simple arguments are rendered within. */
+    private JScrollPane sargument_configuration_scroll;
+    /** the scrolling pane the righthand side is inside - only used for import and build options. the log and log list are not inside a scrollpane */
+    private JScrollPane scroll_pane;
+    /** The message log for the entire session. */
+    private JTextArea log_textarea;
+    /** A tree used to display the currently available option views. */
+    private OptionTree tree = null;
+    /** An array used to pass arguments with dictionary calls. */
+    private String args[] = null;
+    /** The homepage address of the current collection */
+    public String homepage = null;
+
+    /** The constructor creates important helper classes, and initializes all the components.
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     * @see org.greenstone.gatherer.gui.BuildOptions
+     * @see org.greenstone.gatherer.gui.CreatePane.BuildButtonListener
+     * @see org.greenstone.gatherer.gui.CreatePane.CancelButtonListener
+     * @see org.greenstone.gatherer.gui.CreatePane.OptionTree
+     * @see org.greenstone.gatherer.gui.OptionsPane
+     * @see org.greenstone.gatherer.shell.GBuildProgressMonitor
+     * @see org.greenstone.gatherer.shell.GImportProgressMonitor
+     * @see org.greenstone.gatherer.shell.GShellProgressMonitor
+     */
+    public CreatePane() {
+        this.setComponentOrientation(Dictionary.getOrientation());
+	log_textarea = new JTextArea();
+	log_scroll = new JScrollPane(log_textarea);
+        
+	// Create components
+	card_layout = new CardLayout();
+	// Control Pane
+	control_pane = new JPanel();
+        control_pane.setComponentOrientation(Dictionary.getOrientation());
+	tree = new OptionTree();
+        tree.setComponentOrientation(Dictionary.getOrientation());
+	button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+
+	// Progress Pane
+	progress_pane = new JPanel(); // One owner of the bar component
+        progress_pane.setComponentOrientation(Dictionary.getOrientation());
+	bar_area = new JPanel(); // This component will be shared about
+        bar_area.setComponentOrientation(Dictionary.getOrientation());
+
+	progress_import_label = new JLabel(Dictionary.get("CreatePane.Import_Progress"));
+	progress_import_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	import_monitor = new GImportProgressMonitor();
+	Gatherer.c_man.registerImportMonitor(import_monitor);
+
+	progress_build_label = new JLabel(Dictionary.get("CreatePane.Build_Progress"));
+	progress_build_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	build_monitor = new GBuildProgressMonitor(import_monitor.getSharedProgress());
+	Gatherer.c_man.registerBuildMonitor(build_monitor);
+
+	//We only need a schedule monitor for text output. No use for a progress bar!
+	
+	//progress_schedule_label = new JLabel(Dictionary.get("CreatePane.Schedule_Progress")); 
+	
+	schedule_monitor = new GScheduleProgressMonitor(); 
+	Gatherer.c_man.registerScheduleMonitor(schedule_monitor); 
+
+    // And the simple panel
+    slower_panel = new JPanel();
+    sbutton_panel = new JPanel();
+    sinner_panel = new JPanel();
+    
+    slower_panel.setComponentOrientation(Dictionary.getOrientation());
+    sbutton_panel.setComponentOrientation(Dictionary.getOrientation());
+    sinner_panel.setComponentOrientation(Dictionary.getOrientation());
+    
+    //Radio buttons
+    incremental_build_radio_button = new JRadioButton(Dictionary.get("CreatePane.Minimal_Build"));
+    incremental_build_radio_button.setComponentOrientation(Dictionary.getOrientation());
+    incremental_build_radio_button.setToolTipText(Dictionary.get("CreatePane.Minimal_Build_Tooltip"));
+    full_build_radio_button = new JRadioButton(Dictionary.get("CreatePane.Full_Build"));
+    full_build_radio_button.setComponentOrientation(Dictionary.getOrientation());
+    full_build_radio_button.setToolTipText(Dictionary.get("CreatePane.Full_Build_Tooltip"));
+    sincremental_build_radio_button = new JRadioButton(Dictionary.get("CreatePane.Minimal_Build"));
+    sincremental_build_radio_button.setComponentOrientation(Dictionary.getOrientation());
+    sincremental_build_radio_button.setToolTipText(Dictionary.get("CreatePane.Minimal_Build_Tooltip"));
+    sfull_build_radio_button = new JRadioButton(Dictionary.get("CreatePane.Full_Build"));
+    sfull_build_radio_button.setComponentOrientation(Dictionary.getOrientation());
+    sfull_build_radio_button.setToolTipText(Dictionary.get("CreatePane.Full_Build_Tooltip"));
+
+    //keep them in sync
+    full_build_radio_button.addActionListener(
+        new ActionListener() { public void actionPerformed(ActionEvent e) {
+            sfull_build_radio_button.setSelected( full_build_radio_button.isSelected() );
+        }});
+    sfull_build_radio_button.addActionListener(
+        new ActionListener() { public void actionPerformed(ActionEvent e) {
+            full_build_radio_button.setSelected( sfull_build_radio_button.isSelected() );
+        }});
+    incremental_build_radio_button.addActionListener(
+        new ActionListener() { public void actionPerformed(ActionEvent e) {
+            sincremental_build_radio_button.setSelected( incremental_build_radio_button.isSelected() );
+        }});
+    sincremental_build_radio_button.addActionListener(
+        new ActionListener() { public void actionPerformed(ActionEvent e) {
+            incremental_build_radio_button.setSelected( sincremental_build_radio_button.isSelected() );
+        }});
+
+	// Buttons
+	BuildButtonListener bbl = new BuildButtonListener();
+	CancelButtonListener cbl = new CancelButtonListener();
+	
+	build_button = new GLIButton(Dictionary.get("CreatePane.Build_Collection"), Dictionary.get("CreatePane.Build_Collection_Tooltip"));
+	build_button.addActionListener(bbl);
+	
+	cancel_button = new GLIButton(Dictionary.get("CreatePane.Cancel_Build"), Dictionary.get("CreatePane.Cancel_Build_Tooltip"));
+	cancel_button.addActionListener(cbl);
+	cancel_button.setEnabled(false);
+	
+	preview_button = new PreviewButton(Dictionary.get("CreatePane.Preview_Collection"), Dictionary.get("CreatePane.Preview_Collection_Tooltip"));
+	//preview_button.addActionListener(pbl);
+	if(Gatherer.c_man != null) {
+	    preview_button.setEnabled(Gatherer.c_man.built());
+	}
+	else {
+	    preview_button.setEnabled(false);
+	}
+	
+	BuildSimpleButtonListener bsbl = new BuildSimpleButtonListener(); 
+	simple_build_button = new GLIButton(Dictionary.get("CreatePane.Build_Collection"), Dictionary.get("CreatePane.Build_Collection_Tooltip"));
+	simple_build_button.addActionListener(bsbl);
+
+	simple_cancel_button = new GLIButton(Dictionary.get("CreatePane.Cancel_Build"), Dictionary.get("CreatePane.Cancel_Build_Tooltip"));
+	simple_cancel_button.addActionListener(cbl);
+	simple_cancel_button.setEnabled(false);
+	
+	simple_preview_button = new PreviewButton(Dictionary.get("CreatePane.Preview_Collection"), Dictionary.get("CreatePane.Preview_Collection_Tooltip"));
+	//simple_preview_button.addActionListener(pbl);
+	if(Gatherer.c_man != null) {
+	    simple_preview_button.setEnabled(Gatherer.c_man.built());
+	}
+	else {
+	    simple_preview_button.setEnabled(false);
+	}
+	
+	bbl = null;
+	bsbl = null; 
+	cbl = null;
+	//pbl = null;
+    }
+
+
+    public void destroy() {
+	if(document != null) {
+	    document.destroy();
+	}
+    }
+    
+    /** This method is called to actually layout the components.
+     * @see org.greenstone.gatherer.Configuration
+     * @see org.greenstone.gatherer.Gatherer
+     */
+    public void display() {
+
+        int current_mode = Configuration.getMode();
+
+        //Complete/incremental build options panel
+        ButtonGroup build_type_group = new ButtonGroup();
+        build_type_group.add(incremental_build_radio_button);
+        build_type_group.add(full_build_radio_button);
+        full_build_radio_button.setSelected(true);
+
+        ButtonGroup sbuild_type_group = new ButtonGroup();
+        sbuild_type_group.add(sincremental_build_radio_button);
+        sbuild_type_group.add(sfull_build_radio_button);
+        sfull_build_radio_button.setSelected(true);
+
+        JPanel build_type_pane = new JPanel(new GridLayout(2,1));
+        build_type_pane.setComponentOrientation(Dictionary.getOrientation());
+        build_type_pane.add(full_build_radio_button);
+        build_type_pane.add(incremental_build_radio_button);
+
+        JPanel sbuild_type_pane = new JPanel(new GridLayout(2,1));
+        sbuild_type_pane.setComponentOrientation(Dictionary.getOrientation());
+        sbuild_type_pane.add(sfull_build_radio_button);
+        sbuild_type_pane.add(sincremental_build_radio_button);
+
+
+        // Build control_pane
+        JPanel left = new JPanel();
+            left.setComponentOrientation(Dictionary.getOrientation());
+        left.setBorder(BorderFactory.createEmptyBorder(0,5,5,5));
+        left.setLayout(new BorderLayout());
+        left.add(tree, BorderLayout.CENTER);
+        left.add(build_type_pane, BorderLayout.SOUTH);
+        //left.add(full_build_radio_button, BorderLayout.SOUTH);
+
+        right = new JPanel();
+            right.setComponentOrientation(Dictionary.getOrientation());
+        right.setBorder(BorderFactory.createEmptyBorder(0,0,5,5));
+        right.setLayout(new BorderLayout());
+
+        JPanel options_area = new JPanel();
+            options_area.setComponentOrientation(Dictionary.getOrientation());
+        options_area.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(5,5,5,5), BorderFactory.createTitledBorder(Dictionary.get("CreatePane.Options_Title"))));
+        options_area.setLayout(new BorderLayout());
+        options_area.add(left, BorderLayout.LINE_START);
+        options_area.add(right, BorderLayout.CENTER);
+
+        button_pane = new JPanel();
+            button_pane.setComponentOrientation(Dictionary.getOrientation());
+        button_pane.setBorder(BorderFactory.createEmptyBorder(5,10,10,10));
+        button_pane.setLayout(new GridLayout(1,4));
+        button_pane.add(build_button);
+        button_pane.add(cancel_button);
+        button_pane.add(preview_button);
+
+        control_pane.setLayout(new BorderLayout());
+        control_pane.add(options_area, BorderLayout.CENTER);
+        control_pane.add(button_pane, BorderLayout.SOUTH);
+
+        // Build progress_pane
+        JPanel labels_pane = new JPanel();
+            labels_pane.setComponentOrientation(Dictionary.getOrientation());
+        labels_pane.setLayout(new GridLayout(2,1,0,5));
+        labels_pane.add(progress_import_label);
+        labels_pane.add(progress_build_label);
+
+        JPanel monitors_pane = new JPanel();
+            monitors_pane.setComponentOrientation(Dictionary.getOrientation());
+        monitors_pane.setLayout(new GridLayout(2,1,0,5));
+        monitors_pane.add(import_monitor.getProgress());
+        monitors_pane.add(build_monitor.getProgress());
+
+        bar_area.setBorder(BorderFactory.createEmptyBorder(10,10,5,10));
+        bar_area.setLayout(new BorderLayout(5,5));
+        bar_area.add(labels_pane, BorderLayout.LINE_START);
+        bar_area.add(monitors_pane, BorderLayout.CENTER);
+
+        progress_pane.setBorder(BorderFactory.createEmptyBorder(20,20,20,20));
+        progress_pane.setLayout(new BorderLayout());
+        progress_pane.add(bar_area, BorderLayout.NORTH);
+        if(current_mode >= THRESHOLD) {
+            progress_pane.add(log_scroll, BorderLayout.CENTER);
+        }
+
+        // Simple panel
+        sbutton_panel.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+        sbutton_panel.setLayout(new GridLayout(1,3));
+        sbutton_panel.add(simple_build_button);
+        sbutton_panel.add(simple_cancel_button);
+        sbutton_panel.add(simple_preview_button);
+
+        JPanel simple_bar_area = new JPanel(new GridLayout(2,1));
+            simple_bar_area.setComponentOrientation(Dictionary.getOrientation());
+        simple_bar_area.setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+        simple_bar_area.add(import_monitor.getSharedProgress());
+        simple_bar_area.add(sbutton_panel);
+
+        sinner_panel.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));
+        sinner_panel.setLayout(new BorderLayout());
+        sinner_panel.add(sbuild_type_pane, BorderLayout.LINE_START);
+        //sinner_panel.add(full_build_radio_button, BorderLayout.WEST);
+        sinner_panel.add(simple_bar_area, BorderLayout.CENTER);
+
+        if(options_pane != null) {
+            sargument_configuration_panel = options_pane.buildImport(null);
+            sargument_configuration_panel = options_pane.buildBuild(sargument_configuration_panel);
+        }
+        else {
+            sargument_configuration_panel = new JPanel();
+        }
+            sargument_configuration_panel.setComponentOrientation(Dictionary.getOrientation());
+        sargument_configuration_scroll = new JScrollPane(sargument_configuration_panel);
+        sargument_configuration_scroll.setPreferredSize(ARGUMENT_SIZE);
+            sargument_configuration_scroll.setComponentOrientation(Dictionary.getOrientation());
+            
+        slower_panel.setLayout(new BorderLayout());
+        slower_panel.add(sinner_panel, BorderLayout.NORTH);
+        if(current_mode < THRESHOLD) {
+            slower_panel.add(log_scroll, BorderLayout.CENTER);
+        }
+
+        simple_panel = new JSplitPane(JSplitPane.VERTICAL_SPLIT, sargument_configuration_scroll, slower_panel);
+            simple_panel.setComponentOrientation(Dictionary.getOrientation());
+        // Main pane
+        main_pane = new JPanel(card_layout);
+            main_pane.setComponentOrientation(Dictionary.getOrientation());
+        if(current_mode < THRESHOLD) { // Simple mode - add first
+            main_pane.add(simple_panel, SIMPLE);
+        }
+        main_pane.add(control_pane, CONTROL);
+        main_pane.add(progress_pane, PROGRESS);
+        if(current_mode >= THRESHOLD) { // Expert mode - add last
+            main_pane.add(simple_panel, SIMPLE);
+        }
+
+        this.setLayout(new BorderLayout());
+        this.add(main_pane, BorderLayout.CENTER);
+    }
+
+    /** This method is called whenever the 'Create' tab is selected from the view bar. It allows this view to perform any preactions required prior to display. In this case this entails gathering up to date information about the status of the collection including number of documents etc.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     * @see org.greenstone.gatherer.gui.CreatePane.OptionTree
+     */
+    public void gainFocus() {
+	if(Configuration.getMode() < THRESHOLD) {
+	    card_layout.show(main_pane, SIMPLE);
+	}
+	else if(!processing) {
+	    // Move the buttons to control
+	    control_pane.add(button_pane, BorderLayout.SOUTH);
+	    card_layout.show(main_pane, CONTROL);
+	}
+	else {
+	    // Move the buttons to progress
+	    progress_pane.add(button_pane, BorderLayout.SOUTH);
+	    card_layout.show(main_pane, PROGRESS);
+	}
+	// Refresh the set of controls.
+	TreePath path = tree.getSelectionPath();
+	tree.clearSelection();
+	tree.setSelectionPath(path);
+
+    }
+
+    /** We are informed when this view pane loses focus so we can update build options. */
+    public void loseFocus() {
+	tree.valueChanged(null);
+    }
+
+    /** All implementation of GShellListener must include this method so the listener can be informed of messages from the GShell.
+     * @param event A <strong>GShellEvent</strong> that contains, amoung other things, the message.
+     */
+    public synchronized void message(GShellEvent event) {
+	// Ignore the messages from RecPlug (used for progress bars)
+	if (event.getMessage().startsWith("import.pl> RecPlug - ")) {
+	    return;
+	}
+	document.appendLine(event.getMessage());
+	log_textarea.setCaretPosition(document.getLengthToNearestLine());
+    }
+
+    /** Called when the detail mode has changed which in turn may cause several import/build configuration arguments to be available/hidden
+     * @param mode the new mode as an int
+     */
+    public void modeChanged(int mode) {
+	// If we are below the complexity threshold ensure the simple controls are being shown
+	if(Configuration.getMode() < THRESHOLD) {
+	    // Update the arguments
+	    simple_panel.remove(sargument_configuration_scroll);
+	    if(options_pane != null) {
+		sargument_configuration_panel = options_pane.buildImport(null);
+		sargument_configuration_panel = options_pane.buildBuild(sargument_configuration_panel);
+	    }
+	    else {
+		sargument_configuration_panel = new JPanel();
+	    }
+	    sargument_configuration_scroll = new JScrollPane(sargument_configuration_panel);
+            sargument_configuration_panel.setComponentOrientation(Dictionary.getOrientation());
+            sargument_configuration_scroll.setComponentOrientation(Dictionary.getOrientation());
+	    sargument_configuration_scroll.setPreferredSize(ARGUMENT_SIZE);
+	    simple_panel.setTopComponent(sargument_configuration_scroll);
+	    // Remove the shared components from the expert panels
+	    progress_pane.remove(log_scroll);
+	    // And add to simple one
+	    slower_panel.add(log_scroll, BorderLayout.CENTER);
+	    // And bring the card to the front
+	    card_layout.show(main_pane, SIMPLE);
+	}
+	// And if we are above the threshold change to the complex controls
+	else {
+	    // Remove the shared components from the simple panel
+	    slower_panel.remove(log_scroll);
+	    // And add then to the expert ones
+	    progress_pane.add(log_scroll, BorderLayout.CENTER);
+	    // And bring the appropriate card to the front
+	    if(!processing) {
+		control_pane.add(button_pane, BorderLayout.SOUTH);
+		card_layout.show(main_pane, CONTROL);
+	    }
+	    else {
+		progress_pane.add(button_pane, BorderLayout.SOUTH);
+		card_layout.show(main_pane, PROGRESS);
+	    }
+	}
+	tree.valueChanged(null); // Ensure tree argument panels are rebuilt
+    }
+
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell begins its task. Implementation side-effect, not actually used.
+     * @param event A <strong>GShellEvent</strong> that contains details of the initial state of the <strong>GShell</strong> before task comencement.
+     */
+    public synchronized void processBegun(GShellEvent event) {
+	// We don't care. We'll get a more acurate start from the progress monitors.
+    }
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell completes its task.
+     * @param event A <strong>GShellEvent</strong> that contains details of the final state of the <strong>GShell</strong> after task completion.
+     */
+    public synchronized void processComplete(GShellEvent event) {
+	DebugStream.println("In CreatePane::processComplete(" + event.getType() + ")...");
+	if(event.getStatus() == GShell.OK) {
+	    if(event.getType() == GShell.SCHEDULE) {
+		
+		processing = false; 
+		build_button.setEnabled(true);
+		cancel_button.setEnabled(false);
+		//preview_button.setEnabled(true);
+		//only enable preview if the collection has been built. 
+		preview_button.setEnabled(Gatherer.c_man.built());
+		simple_build_button.setEnabled(true);
+		simple_cancel_button.setEnabled(false);
+		simple_preview_button.setEnabled(Gatherer.c_man.built()); 
+		int status = event.getStatus();
+		document.setSpecialCharacter(OptionsPane.SCHEDULED); 
+		options_pane.resetFileEntry();
+		build_monitor.reset();
+		import_monitor.reset();
+		if(Configuration.getMode() >= THRESHOLD) {
+		    control_pane.add(button_pane, BorderLayout.SOUTH); 
+		    card_layout.show(main_pane, CONTROL); 
+		}
+	    }
+	    else if(event.getType() == GShell.BUILD) {
+		processing = false;
+		build_button.setEnabled(true);
+		cancel_button.setEnabled(false);
+		preview_button.setEnabled(Gatherer.c_man.built());
+		simple_build_button.setEnabled(true);
+		simple_cancel_button.setEnabled(false);
+		simple_preview_button.setEnabled(Gatherer.c_man.built());
+		int status = event.getStatus();
+		document.setSpecialCharacter(OptionsPane.SUCCESSFUL);
+		options_pane.resetFileEntry();
+		build_monitor.reset();
+		import_monitor.reset();
+		if(Configuration.getMode() >= THRESHOLD) {
+		    control_pane.add(button_pane, BorderLayout.SOUTH);
+		    card_layout.show(main_pane, CONTROL);
+		}
+	    }
+	    // Otherwise its completed import but still got build to go
+	}
+	else {
+	    processing = false;
+	    cancel_button.setEnabled(false);
+	    build_button.setEnabled(true);
+	    // The build may have failed, but a previous index may still be in place
+	    preview_button.setEnabled(Gatherer.c_man.built());
+
+	    simple_build_button.setEnabled(true);
+	    simple_cancel_button.setEnabled(false);
+	    simple_preview_button.setEnabled(Gatherer.c_man.built());
+	    if(event.getStatus() == GShell.CANCELLED) {
+		document.setSpecialCharacter(OptionsPane.CANCELLED);
+	    }
+	    else {
+		document.setSpecialCharacter(OptionsPane.UNSUCCESSFUL);
+	    }
+	    options_pane.resetFileEntry();
+	    if(Configuration.getMode() >= THRESHOLD) {
+		control_pane.add(button_pane, BorderLayout.SOUTH);
+		card_layout.show(main_pane, CONTROL);
+	    }
+	}
+    }
+
+
+    /** This method is invoked at any time there has been a significant change in the collection, such as saving, loading or creating.
+     * @param ready A <strong>boolean</strong> indicating if a collection is currently available.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.CollectionManager
+     * @see org.greenstone.gatherer.gui.BuildOptions
+     * @see org.greenstone.gatherer.gui.OptionsPane
+     */
+    public void refresh(int refresh_reason, boolean ready)
+    {
+	if (Gatherer.c_man == null || !ready) {
+	    return;
+	}
+	Collection current_collection = Gatherer.c_man.getCollection();
+	if (current_collection != previous_collection && !Gatherer.c_man.isImporting()) {
+	    this.options_pane = new OptionsPane(current_collection.import_options, current_collection.build_options, current_collection.schedule_options);
+	    if (previous_collection != null) {
+		// clear the log
+		Document log_document = log_textarea.getDocument();
+		if (log_document instanceof AppendLineOnlyFileDocument) {
+		    ((AppendLineOnlyFileDocument) log_document).destroy();
+		}
+	    }
+	    previous_collection = current_collection;
+	   
+	} 
+	// If we are in simple mode, we have to rebuild the simple arguments list
+	if(Configuration.getMode() < THRESHOLD) {
+	    simple_panel.remove(sargument_configuration_scroll);
+	    sargument_configuration_panel = options_pane.buildImport(null);
+	    sargument_configuration_panel = options_pane.buildBuild(sargument_configuration_panel);
+	    //if(CollectionManager.canDoScheduling()) {
+	    //sargument_configuration_panel = options_pane.buildSchedule(sargument_configuration_panel); 
+	    //}
+	    sargument_configuration_scroll = new JScrollPane(sargument_configuration_panel);
+            sargument_configuration_scroll.setComponentOrientation(Dictionary.getOrientation());
+	    sargument_configuration_scroll.setPreferredSize(ARGUMENT_SIZE);
+	    simple_panel.setTopComponent(sargument_configuration_scroll);
+	}
+	tree.valueChanged(null);
+
+	// Validate the preview button
+	if (Gatherer.c_man.built() && Configuration.library_url != null) {
+	    preview_button.setEnabled(true);
+	    simple_preview_button.setEnabled(true);
+	}
+	else {
+	    preview_button.setEnabled(false);
+	    simple_preview_button.setEnabled(false);
+	}
+    }
+
+    /** This class serves as the listener for actions on the build button. */
+    private class BuildButtonListener
+	implements ActionListener {
+	/**
+	 * This method checks to see what needs to be done for a build, and starts the process off.
+	 * A complete build proccess is started by {@link CollectionManager.importCollection()}, which then imports and builds the collection.
+	 * However, this doesn't always happen, as sometimes an incremental build will do :-)
+	 * @param event An <strong>ActionEvent</strong> who, thanks to the power of object oriented programming, we don't give two hoots about.
+	 * @see org.greenstone.gatherer.Gatherer
+	 * @see org.greenstone.gatherer.collection.CollectionManager
+	 * @see org.greenstone.gatherer.gui.BuildOptions
+	 * @see org.greenstone.gatherer.shell.GShellProgressMonitor
+	 */
+    public void actionPerformed(ActionEvent event) {
+        Collection collection = Gatherer.c_man.getCollection();
+        String collection_name = collection.getName();
+
+        //if button is labelled for Building
+        if(event.getActionCommand().equals(Dictionary.get("CreatePane.Build_Collection"))) { 
+
+            //set a static variable marking whether this is a "Complete" or "Minimal" build
+            CollectionDesignManager.setCompleteBuild( full_build_radio_button.isSelected() );
+
+            //do the import and build, skipping import if possible
+            /*if( !CollectionDesignManager.isCompleteBuild() &&
+                !collection.getMetadataChanged() && !collection.getFilesChanged() && Gatherer.c_man.imported() &&
+                CollectionDesignManager.getRebuildTypeRequired() <= CollectionDesignManager.BUILDCOL
+                ) {
+                    // Just do build (no import)
+                    DebugStream.println("Just want to run buildcol.pl");
+                    prepareForBuild();
+                    Gatherer.c_man.buildCollection();
+            }
+            else {
+			*/
+                prepareForBuild();
+
+    	// Starting building,
+	// For now, for a GS3 solr collection, we need to stop the GS3 server before building
+	// and restart it after building is complete. Restart is done in CollectionManager.processComplete()
+	if(CollectionManager.isSolrCollection()) {
+	    /*GS3ServerThread thread = new GS3ServerThread(Configuration.gsdl3_src_path, "stop");
+	      thread.start();*/
+	}
+
+                Gatherer.c_man.importCollection(); //This starts the building process.
+			/*
+            }
+			*/
+
+        } else { //button is labelled for setting up scheduling
+
+            //this needs to be called to configure buttons... but no building is done
+            prepareForBuild(); 
+            Gatherer.c_man.scheduleBuild(); 
+        }
+        //Re-setting the rebuildTypeRequired is handled by CollectionManager.processComplete(GShellEvent)
+    }
+
+	    
+	/**
+	 * This does some stuff that is needed before a collection can be built.
+	 * For example, buttons are enabled/disabled, and certain flags are set.
+	 * This is called from {@link actionPerformed(ActionEvent)}
+	 */
+	private void prepareForBuild() {
+	    // First we force the build options to be updated if we haven't already.
+	    tree.valueChanged(null);
+	    
+	    // Remember that for lower thresholds the above doesn't work, so try this instead
+	    if(Configuration.getMode() < THRESHOLD) {
+		options_pane.update(sargument_configuration_panel);
+	    }
+	    
+	    // Now go about building.
+	    build_button.setEnabled(false);
+	    cancel_button.setEnabled(true);
+	    preview_button.setEnabled(false);
+	    
+	    simple_build_button.setEnabled(false);
+	    simple_cancel_button.setEnabled(true);
+	    simple_preview_button.setEnabled(false);
+	    
+	    document = options_pane.createNewLogDocument();
+	    log_textarea.setDocument(document);
+	    options_pane.log_textarea.setDocument(document);
+	    // Change the view.
+	    processing = true;
+	    if(Configuration.getMode() >= THRESHOLD) {
+		progress_pane.add(button_pane, BorderLayout.SOUTH);
+		card_layout.show(main_pane, PROGRESS);
+	    }
+	    // Reset the stop flag in all the process monitors, just incase.
+	    ((GBuildProgressMonitor)build_monitor).reset();
+	    import_monitor.setStop(false);
+	}
+    }
+    
+    /** I hope this works. Wendy */ 
+    private class BuildSimpleButtonListener
+	implements ActionListener {
+	/**
+	 * This method checks to see what needs to be done for a build, and starts the process off.
+	 * A complete build proccess is started by {@link CollectionManager.importCollection()}, which then imports and builds the collection.
+	 * However, this doesn't always happen, as sometimes an incremental build will do :-)
+	 * @param event An <strong>ActionEvent</strong> who, thanks to the power of object oriented programming, we don't give two hoots about.
+	 * @see org.greenstone.gatherer.Gatherer
+	 * @see org.greenstone.gatherer.collection.CollectionManager
+	 * @see org.greenstone.gatherer.gui.BuildOptions
+	 * @see org.greenstone.gatherer.shell.GShellProgressMonitor
+	 */
+	public void actionPerformed(ActionEvent event) {
+		Collection collection = Gatherer.c_man.getCollection();
+		String collection_name = collection.getName();
+
+		//set a static variable marking whether this is a "Complete" or "Minimal" build
+		CollectionDesignManager.setCompleteBuild( full_build_radio_button.isSelected() );
+
+		//do the import and build, skipping import if possible
+		/*if( !CollectionDesignManager.isCompleteBuild() &&
+			!collection.getMetadataChanged() && !collection.getFilesChanged() && Gatherer.c_man.imported() &&
+			CollectionDesignManager.getRebuildTypeRequired() <= CollectionDesignManager.BUILDCOL
+			) {
+				// Just do build (no import)
+				DebugStream.println("Just want to run buildcol.pl");
+				prepareForBuild();
+				Gatherer.c_man.buildCollection();
+		}
+		else {
+		*/
+			prepareForBuild();
+			Gatherer.c_man.importCollection(); //This starts the building process.
+		/*
+		}
+		*/
+		
+		//Re-setting the rebuildTypeRequired is handled by CollectionManager.processComplete(GShellEvent)
+	}
+
+	/**
+	 * This does some stuff that is needed before a collection can be built.
+	 * For example, buttons are enabled/disabled, and certain flags are set.
+	 * This is called from {@link actionPerformed(ActionEvent)}
+	 */
+	private void prepareForBuild() {
+	    // First we force the build options to be updated if we haven't already.
+	    tree.valueChanged(null);
+	    
+	    // Remember that for lower thresholds the above doesn't work, so try this instead
+	    if(Configuration.getMode() < THRESHOLD) {
+		options_pane.update(sargument_configuration_panel);
+	    }
+	    
+	    // Now go about building.
+	    build_button.setEnabled(false);
+	    cancel_button.setEnabled(true);
+	    preview_button.setEnabled(false);
+	    
+	    simple_build_button.setEnabled(false);
+	    simple_cancel_button.setEnabled(true);
+	    simple_preview_button.setEnabled(false);
+	    
+	    document = options_pane.createNewLogDocument();
+	    log_textarea.setDocument(document);
+	    options_pane.log_textarea.setDocument(document);
+	    // Change the view.
+	    processing = true;
+	    if(Configuration.getMode() >= THRESHOLD) {
+		progress_pane.add(button_pane, BorderLayout.SOUTH);
+		card_layout.show(main_pane, PROGRESS);
+	    }
+	    // Reset the stop flag in all the process monitors, just incase.
+	    ((GBuildProgressMonitor)build_monitor).reset();
+	    import_monitor.setStop(false);
+	}
+    }
+    
+
+    /** This class serves as the listener for actions on the cancel button. */
+    private class CancelButtonListener
+	implements ActionListener {
+	/** This method attempts to cancel the current GShell task. It does this by first telling CollectionManager not to carry out any further action. This it turn tells the GShell to break from the current job immediately, without waiting for the processEnded message, and then kills the thread in an attempt to stop the process. The results of such an action are debatable.
+	 * @param event An <strong>ActionEvent</strong> who, thanks to the power of object oriented programming, we don't give two hoots about.
+	 * @see org.greenstone.gatherer.Gatherer
+	 * @see org.greenstone.gatherer.collection.CollectionManager
+	 * @see org.greenstone.gatherer.gui.BuildOptions
+	 * @see org.greenstone.gatherer.shell.GShellProgressMonitor
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    build_button.setEnabled(false);
+	    cancel_button.setEnabled(false);
+	    preview_button.setEnabled(false);
+
+	    simple_build_button.setEnabled(false);
+	    simple_cancel_button.setEnabled(false);
+	    simple_preview_button.setEnabled(false);
+
+	    processing = false;
+	    document.setSpecialCharacter(OptionsPane.CANCELLED);
+	    if(Configuration.getMode() >= THRESHOLD) {
+		control_pane.add(button_pane, BorderLayout.SOUTH);
+		card_layout.show(main_pane, CONTROL);
+	    }
+	    // Set the stop flag in all the process monitor.
+	    import_monitor.setStop(true);
+	    import_monitor.reset();
+	    build_monitor.setStop(true);
+	    build_monitor.reset();
+
+	    // Remove the collection lock.
+	    //Gatherer.g_man.lockCollection(false, false);
+
+       	// Cancelling building:
+	// For now, for a GS3 solr collection, we need to stop the GS3 server before building
+	// and restart it after building is complete. If a cancel is pressed during build
+	// then we also restart the GS3 server
+	if(CollectionManager.isSolrCollection()) {
+	    /*GS3ServerThread thread = new GS3ServerThread(Configuration.gsdl3_src_path, "restart");
+	      thread.start();*/
+	}
+
+
+	}
+    }
+
+    
+    /** The OptionTree is simply a tree structure that has a root node labelled "Options" and then has a child node for each available options screen. These screens are either combinations of the available import and build arguments, or a message log detailing the shell processes progress. */
+    private class OptionTree
+	extends JTree
+	implements TreeSelectionListener {
+	/** The model behind the tree. */
+	private DefaultTreeModel model = null;
+	/** The previous options view displayed, which is sometimes needed to refresh properly. */
+	private JPanel previous_pane = null;
+	/** The node corresponding to the building options view. */
+	private OptionTreeNode building  = null;
+	/** The node corresponding to the importing options view. */
+	private OptionTreeNode importing = null;
+	/** The node corresponding to the scheduling options view. */ 
+	private OptionTreeNode scheduling = null; 
+	/** The node corresponding to the log view. */
+	private OptionTreeNode log       = null;
+	/** The root node of the options tree, which has no associated options view. */
+	private OptionTreeNode options   = null;
+	/** Constructor.
+	 * @see org.greenstone.gatherer.gui.CreatePane.OptionTreeNode
+	 */
+	public OptionTree() {
+	    super();
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    ToolTipManager.sharedInstance().registerComponent(this);
+	    addTreeSelectionListener(this);
+	    getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+	    setCellRenderer(new ToolTipTreeCellRenderer());
+	    setRootVisible(false);
+	    setToggleClickCount(1);
+
+	    // Create tree.
+	    building = new OptionTreeNode(Dictionary.get("CreatePane.Build"));
+	    building.setToolTipText(Dictionary.get("CreatePane.Build_Tooltip"));
+	    importing = new OptionTreeNode(Dictionary.get("CreatePane.Import"));
+	    importing.setToolTipText(Dictionary.get("CreatePane.Import_Tooltip"));
+	    if (CollectionManager.canDoScheduling()) {
+	      scheduling = new OptionTreeNode(Dictionary.get("CreatePane.Schedule"));
+	      scheduling.setToolTipText(Dictionary.get("CreatePane.Schedule_Tooltip")); 
+
+	    }
+	    log = new OptionTreeNode(Dictionary.get("CreatePane.Log"));
+	    log.setToolTipText(Dictionary.get("CreatePane.Log_Tooltip"));
+	    options = new OptionTreeNode(Dictionary.get("CreatePane.Options"));
+
+	    model = new DefaultTreeModel(options);
+	    setModel(model);
+	    model.insertNodeInto(importing, options, 0);
+	    model.insertNodeInto(building, options, 1);
+	    if (CollectionManager.canDoScheduling()) {
+	      model.insertNodeInto(scheduling, options, 2); 
+	      model.insertNodeInto(log, options, 3);
+	    } else {
+	      model.insertNodeInto(log, options, 2);
+	    }
+	    // Expand the root node
+	    expandPath(new TreePath(options));
+	}
+	/** Any implementation of TreeSelectionListener must include this method to allow this listener to know when the selection has changed. Here we swap the options view depending on the selected OptionTreeNode.
+	 * @param event A <Strong>TreeSelectionEvent</strong> which contains all the information garnered when the event occured.
+	 * @see org.greenstone.gatherer.gui.CreatePane.OptionTreeNode
+	 */
+	public void valueChanged(TreeSelectionEvent event) {
+	    TreePath path = null;
+	    OptionTreeNode node = null;
+	    //if(event != null) {
+	    //path = event.getPath();
+	    path = getSelectionPath();
+	    if(path != null) {
+		node = (OptionTreeNode)path.getLastPathComponent();
+	    }
+				//}
+	    if(previous_pane != null) {
+		//target_pane.remove(previous_pane);
+		options_pane.update(previous_pane);
+		if(scroll_pane != null) {
+		    right.remove(scroll_pane);
+		}
+		else {
+		    right.remove(previous_pane);
+		}
+		previous_pane = null;
+		scroll_pane = null;
+	    }
+	    if(node != null && node.equals(log)) { 
+		build_button.setActionCommand(Dictionary.get("CreatePane.Build_Collection")); 
+		build_button.setText(Dictionary.get("CreatePane.Build_Collection")); 	
+	    }
+	    if(node != null && node.equals(building)) {
+	      build_button.setActionCommand(Dictionary.get("CreatePane.Build_Collection"));		
+		build_button.setText(Dictionary.get("CreatePane.Build_Collection"));
+ 
+
+		previous_pane = options_pane.buildBuild(null);
+		scroll_pane = new JScrollPane(previous_pane);
+		right.add(scroll_pane, BorderLayout.CENTER);
+		//target_pane.add(previous_pane, BorderLayout.CENTER);
+	    }
+	    else if(node != null && node.equals(importing)) {
+		build_button.setActionCommand(Dictionary.get("CreatePane.Build_Collection")); 
+		build_button.setText(Dictionary.get("CreatePane.Build_Collection")); 
+
+		previous_pane = options_pane.buildImport(null);
+		scroll_pane = new JScrollPane(previous_pane);
+		right.add(scroll_pane, BorderLayout.CENTER);
+		//target_pane.add(previous_pane, BorderLayout.CENTER);
+	    }
+	    else if(node != null && CollectionManager.canDoScheduling() && node.equals(scheduling)) { 
+		build_button.setActionCommand(Dictionary.get("CreatePane.Schedule_Build")); 
+		build_button.setText(Dictionary.get("CreatePane.Schedule_Build"));
+ 
+		previous_pane = options_pane.buildSchedule(null); 
+		scroll_pane = new JScrollPane(previous_pane);
+		right.add(scroll_pane, BorderLayout.CENTER);
+	    }
+	    else {
+		if (options_pane != null) {
+		    previous_pane = options_pane.buildLog();
+		    right.add(previous_pane, BorderLayout.CENTER);
+		    right.updateUI(); // we need to repaint the log pane, cos it hasn't changed since last time
+		    ///ystem.err.println("I've added the log back to the right pane again.");
+		    //target_pane.add(previous_pane, BorderLayout.CENTER);
+		}
+	    }
+	    //scroll_pane.setViewportView(previous_pane);
+	    //previous_pane.validate();
+	    right.validate();
+	    //System.err.println("Current pane size: " + previous_pane.getSize());
+	    //System.err.println("While its preferred size is: " + previous_pane.getPreferredSize());
+	}
+    }
+
+    /** The <strong>OptionTree</strong> is built from these nodes, each of which has several methods used in creating the option panes.
+     */
+    private class OptionTreeNode
+	extends DefaultMutableTreeNode
+	implements Comparable {
+	/** The text label given to this node in the tree. */
+	private String title = null;
+	/** a tool tip to be used for this node in the tree */
+	private String tool_tip = null;
+	/** Constructor.
+	 * @param title The <strong>String</strong> which serves as this nodes title.
+	 */
+	public OptionTreeNode(String title) {
+	    super();
+	    this.title = title;
+	}
+
+	/** This method compares two nodes for ordering.
+	 * @param obj The <strong>Object</strong> to compare to.
+	 * @return An <strong>int</strong> of one of the possible values -1, 0 or 1 indicating if this node is less than, equal to or greater than the target node respectively.
+	 */
+	public int compareTo(Object obj) {
+	    return title.compareTo(obj.toString());
+	}
+
+	/** This method checks if two nodes are equivalent.
+	 * @param obj The <strong>Object</strong> to be tested against.
+	 * @return A <strong>boolean</strong> which is <i>true</i> if the objects are equal, <i>false</i> otherwise.
+	 */
+	public boolean equals(Object obj) {
+	    if(compareTo(obj) == 0) {
+		return true;
+	    }
+	    return false;
+	}
+
+	/** get the tool tip */
+	public String getToolTipText() {
+	    return tool_tip;
+	}
+
+	/** set the tool tip */
+	public void setToolTipText(String tip) {
+	    tool_tip = tip;
+	}
+	
+	/** Method to translate this node into a textual representation.
+	 * @return A <strong>String</strong> which in this case is the title.
+	 */
+	public String toString() {
+	    return title;
+	}
+    }
+
+    // Adds tooltips to the tree nodes
+    private class ToolTipTreeCellRenderer
+	extends DefaultTreeCellRenderer {
+
+	public Component getTreeCellRendererComponent(JTree tree,
+						      Object value,
+						      boolean sel,
+						      boolean expanded,
+						      boolean leaf,
+						      int row,
+						      boolean hasFocus) {
+
+	    super.getTreeCellRendererComponent(tree, value, sel,
+					       expanded, leaf, row,
+					       hasFocus);
+	    if (value instanceof OptionTreeNode) {
+		String tip = ((OptionTreeNode) value).getToolTipText();
+		if (tip != null) {
+		    setToolTipText(tip);
+		}
+	    }
+	    return this;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/CreateShortcutPrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/CreateShortcutPrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/CreateShortcutPrompt.java	(revision 31635)
@@ -0,0 +1,151 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.file.WorkspaceTree;
+
+
+public class CreateShortcutPrompt	
+    extends JDialog
+    implements ActionListener, KeyListener
+{
+    /** The default size of a special mapping dialog. */
+    static final Dimension DIALOG_SIZE = new Dimension(400, 120);
+
+    private boolean cancelled = false;
+    private JButton cancel_button = null;
+    private JButton ok_button = null;
+    private JTextField name_field = null;
+
+
+    public CreateShortcutPrompt(WorkspaceTree workspace_tree, File file)
+    {
+	super(Gatherer.g_man);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	setModal(true);
+	setSize(DIALOG_SIZE);
+	setTitle(Dictionary.get("MappingPrompt.Title"));
+	
+	// Creation
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel center_pane = new JPanel();
+        center_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel labels_pane = new JPanel();
+        labels_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel fields_pane = new JPanel();
+        fields_pane.setComponentOrientation(Dictionary.getOrientation());
+
+	JLabel file_label = new JLabel(Dictionary.get("MappingPrompt.File"));
+	file_label.setComponentOrientation(Dictionary.getOrientation());
+        JLabel file_field = new JLabel(file.getAbsolutePath());
+        file_field.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel name_label = new JLabel(Dictionary.get("MappingPrompt.Name"));
+	name_label.setComponentOrientation(Dictionary.getOrientation());                
+        name_field = new JTextField(file.getName());
+        name_field.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+	ok_button.setEnabled(name_field.getText().length() > 0);
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+	
+	// Connection
+	cancel_button.addActionListener(this);
+	ok_button.addActionListener(this);
+	name_field.addKeyListener(this);
+
+	// Layout
+	labels_pane.setLayout(new GridLayout(2,1,5,0));
+	labels_pane.add(file_label);
+	labels_pane.add(name_label);
+    
+	fields_pane.setLayout(new GridLayout(2,1,5,0));
+	fields_pane.add(file_field);
+	fields_pane.add(name_field);
+
+	center_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+	center_pane.setLayout(new BorderLayout(5,0));
+	center_pane.add(labels_pane, BorderLayout.LINE_START);
+	center_pane.add(fields_pane, BorderLayout.CENTER);
+
+	button_pane.setLayout(new GridLayout(1,2,5,5));
+	button_pane.add(ok_button);
+	button_pane.add(cancel_button);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(center_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	// Display
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - DIALOG_SIZE.width) / 2, (screen_size.height - DIALOG_SIZE.height) / 2);
+	setVisible(true);
+
+	// If not cancelled create mapping.
+	if (!cancelled) {
+	    workspace_tree.addDirectoryMapping(name_field.getText(), file);
+	}
+    }
+
+
+    public void actionPerformed(ActionEvent event)
+    {
+	if (event.getSource() == cancel_button) {
+	    cancelled = true;
+	}
+	dispose();
+    }
+
+
+    public void destroy()
+    {
+	cancel_button = null;
+	ok_button = null;
+	name_field = null;
+    }
+
+
+    public void keyPressed(KeyEvent event) { }
+
+    public void keyReleased(KeyEvent event)
+    {
+	ok_button.setEnabled(name_field.getText().length() > 0);
+    }
+
+    public void keyTyped(KeyEvent event) { }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DeleteCollectionPrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DeleteCollectionPrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DeleteCollectionPrompt.java	(revision 31635)
@@ -0,0 +1,360 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.net.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.BasicCollectionConfiguration;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.file.WorkspaceTree;  // !!! Don't like this here
+import org.greenstone.gatherer.util.ArrayTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+/** This class provides the functionality to delete current collections from the GSDLHOME/collect/ directory. The user chooses the collection from a list, where each entry also displays details about itself, confirms the delete of a collection by checking a checkbox then presses the ok button to actually delete the collection.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class DeleteCollectionPrompt
+    extends ModalDialog {
+    /** The currently selected collection for deletion. */
+    private BasicCollectionConfiguration collection = null;
+    /** The model behind the list. */
+    private DefaultListModel list_model = null;
+    /** A reference to ourself so any inner-classes can dispose of us. */
+    private DeleteCollectionPrompt prompt = null;
+    /** The close button, which exits the prompt without deleting anything. */
+    private JButton close_button = null;
+    /** The ok button which causes the selected collection to be deleted. */
+    private JButton ok_button = null;
+    /** The confirmation check box. */
+    private JCheckBox confirmation = null;
+    /** The label above details. */
+    private JLabel details_label = null;
+    /** The label above the list. */
+    private JLabel list_label = null;
+    /** The list of available collections. */
+    private JList list = null;
+    /** The text area used to display details about the collection selected. */
+    private JTextArea details = null;
+    /** A string array used to pass arguments to the phrase retrieval method. */
+    private String args[] = null;
+
+    private boolean current_coll_deleted = false;
+    /** The size of the delete prompt screen. */
+    public static final Dimension SIZE = new Dimension(500, 500);
+
+    /** Constructor.
+     */
+    public DeleteCollectionPrompt() {
+	super(Gatherer.g_man);
+	close_button = new GLIButton(Dictionary.get("General.Close"), Dictionary.get("General.Close_Tooltip"));
+	this.setComponentOrientation(Dictionary.getOrientation());
+	confirmation = new JCheckBox(Dictionary.get("DeleteCollectionPrompt.Confirm_Delete"));
+	confirmation.setComponentOrientation(Dictionary.getOrientation());
+        details = new JTextArea(Dictionary.get("DeleteCollectionPrompt.No_Collection"));
+        details.setComponentOrientation(Dictionary.getOrientation());
+	details.setEditable(false);
+	details_label = new JLabel(Dictionary.get("DeleteCollectionPrompt.Collection_Details"));
+        details_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	list = new JList();
+        list.setComponentOrientation(Dictionary.getOrientation());
+	list_label = new JLabel(Dictionary.get("DeleteCollectionPrompt.Collection_List"));
+	list_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	list_model = new DefaultListModel();
+	ok_button = new GLIButton(Dictionary.get("DeleteCollectionPrompt.Delete"), Dictionary.get("DeleteCollectionPrompt.Delete_Tooltip"));
+	
+	prompt = this;
+	setModal(true);
+	setSize(SIZE);
+	setTitle(Dictionary.get("DeleteCollectionPrompt.Title"));	        
+        
+	setJMenuBar(new SimpleMenuBar("deletingcollections")); 
+	close_button.addActionListener(new CloseButtonListener());
+	confirmation.addActionListener(new ConfirmationCheckBoxListener());
+	confirmation.setEnabled(false);
+	confirmation.setSelected(false);
+	list.addListSelectionListener(new CollectionListListener());
+	list.clearSelection();
+	list.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+	list.setModel(list_model);
+	ok_button.addActionListener(new OKButtonListener());
+	ok_button.setEnabled(false);
+	scanForCollections();
+        
+    }
+
+    /** Destructor. */
+    public void destroy() {
+	list_model.clear();
+	list_model = null;
+	close_button = null;
+	confirmation = null;
+	details = null;
+	details_label = null;
+	list = null;
+	ok_button = null;
+	prompt = null;
+    }
+
+    /** This method causes the modal prompt to be displayed. 
+     * returns true if it has deleted the collection that is currently open */
+    public boolean display() {
+	// Central pane
+        JScrollPane scrol_tmp;
+	JPanel list_pane = new JPanel(new BorderLayout());
+        list_pane.setComponentOrientation(Dictionary.getOrientation());
+	list_pane.add(list_label, BorderLayout.NORTH);
+        scrol_tmp = new JScrollPane(list);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	list_pane.add(scrol_tmp, BorderLayout.CENTER);
+	list_pane.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
+
+	JPanel details_pane = new JPanel(new BorderLayout());
+        details_pane.setComponentOrientation(Dictionary.getOrientation());
+	details_pane.add(details_label, BorderLayout.NORTH);
+        scrol_tmp = new JScrollPane(details);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	details_pane.add(scrol_tmp, BorderLayout.CENTER);
+	details_pane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
+
+	JPanel central_pane = new JPanel(new GridLayout(2, 1));
+        central_pane.setComponentOrientation(Dictionary.getOrientation());
+	central_pane.add(list_pane);
+	central_pane.add(details_pane);
+	central_pane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+
+	// Lower pane
+	JPanel confirmation_pane = new JPanel(new BorderLayout());
+        confirmation_pane.setComponentOrientation(Dictionary.getOrientation());
+	confirmation_pane.add(confirmation, BorderLayout.CENTER);
+	confirmation_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+
+	JPanel button_pane = new JPanel(new GridLayout(1, 2));
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	button_pane.add(ok_button);
+	button_pane.add(close_button);
+	button_pane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
+
+	JPanel lower_pane = new JPanel(new BorderLayout());
+        lower_pane.setComponentOrientation(Dictionary.getOrientation());
+	lower_pane.add(confirmation_pane, BorderLayout.NORTH);
+	lower_pane.add(button_pane, BorderLayout.SOUTH);
+	lower_pane.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5));
+
+	// Final.
+	JPanel content_pane = (JPanel)this.getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(central_pane, BorderLayout.CENTER);
+	content_pane.add(lower_pane, BorderLayout.SOUTH);
+
+	// Center and display.
+	Dimension screen_size = Configuration.screen_size;
+	this.setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	this.setVisible(true); // blocks until the dialog is killed
+	return current_coll_deleted;
+    }
+
+
+    /** Shows a delete complete prompt. 
+     * @param success A <strong>boolean</strong> indicating if the collection was successfully deleted.
+     * @see org.greenstone.gatherer.collection.Collection
+     */
+    public void resultPrompt(boolean success) {
+	args = new String[1];
+	args[0] = collection.getName();
+	if (success) {
+	    JOptionPane.showMessageDialog(prompt,Dictionary.get("DeleteCollectionPrompt.Successful_Delete", args),Dictionary.get("DeleteCollectionPrompt.Successful_Title"),JOptionPane.INFORMATION_MESSAGE);
+	}
+	else {
+	    JOptionPane.showMessageDialog(prompt,Dictionary.get("DeleteCollectionPrompt.Failed_Delete", args),Dictionary.get("DeleteCollectionPrompt.Failed_Title"),JOptionPane.WARNING_MESSAGE);
+	}
+    }
+
+    /** Method to scan the collect directory retrieving and reloading each collection it finds, while building the list of known collections.
+     * @see org.greenstone.gatherer.Configuration
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.util.ArrayTools
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    private void scanForCollections() {
+	// Start at the collect dir.
+	File collect_directory = new File(Gatherer.getCollectDirectoryPath());
+	if (collect_directory.exists()) {
+	  loadCollectionConfigs(collect_directory);
+	}
+    }
+
+  private void loadCollectionConfigs(File directory) {
+    // For each child directory see if it contains an 
+    // etc/collect.cfg file and if so try to load it..
+    File collections[] = directory.listFiles();
+    String file_name = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+    ArrayTools.sort(collections);
+    for(int i = 0; collections != null && i < collections.length; i++) {
+      if(collections[i].isDirectory() && !collections[i].getName().equals(StaticStrings.MODEL_COLLECTION_NAME)) {
+	File config_file = new File(collections[i], file_name);
+	if (config_file.exists()) {
+	  BasicCollectionConfiguration config = new BasicCollectionConfiguration(config_file);
+	  if (config.getCollectGroup().equals("true")) {
+	    loadCollectionConfigs(collections[i]);
+	  } else {
+	    list_model.addElement(config);
+	    config = null;
+	  }
+	}
+      }
+    }
+  }
+  
+
+    /** A button listener implementation, which listens for actions on the close button and disposes of the dialog when detected. */
+    private class CloseButtonListener 
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so we can be informed when the button is actioned.
+	 * @param event An <strong>ActionEvent</strong> containing all the relevant information garnered from the event itself.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    // Done
+	    prompt.dispose();
+	}
+    }
+
+    /** This private class listens for selection events in from the list and then displays the appropriate details for that collection.
+     */
+    private class CollectionListListener 
+	implements ListSelectionListener {
+	/** Any implementation of ListSelectionListener must include this method so we can be informed when the list selection changes.
+	 * @param  event a <strong>ListSelectionEvent</strong> containing all the relevant information garnered from the event itself
+	 */
+	public void valueChanged(ListSelectionEvent event) {
+	    ok_button.setEnabled(false);
+	    confirmation.setSelected(false);
+	    if(!list.isSelectionEmpty()) {
+		collection = (BasicCollectionConfiguration) list.getSelectedValue();
+
+		String currentColName = (Gatherer.c_man.getCollection() == null) ? "" : Gatherer.c_man.getCollection().getName();
+		if(!collection.getShortName().equals(currentColName)) { 
+		    // the selected collection can only be deleted if it is NOT the collection that is currently open
+		    confirmation.setEnabled(true);
+		    confirmation.setToolTipText(null); // no tooltip
+		    ok_button.setToolTipText(null);
+
+		    args = new String[3];
+		    args[0] = collection.getCreator();
+		    args[1] = collection.getMaintainer();
+		    args[2] = collection.getDescription();
+		    details.setText(Dictionary.get("DeleteCollectionPrompt.Details", args));
+		    details.setCaretPosition(0);
+		} else { // trying to delete the collection that is open at present
+		    // add a tooltip saying to the confirmation checkbox saying that the current collection can't be deleted 
+		    String tooltip = Dictionary.get("DeleteCollectionPrompt.Cannot_Delete_Open_Collection_Tooltip", 
+						    collection.getName());
+		    confirmation.setToolTipText(tooltip);
+		    ok_button.setToolTipText(tooltip);
+		    // delete/ok button and confirmation checkbox should be disabled
+		    ok_button.setEnabled(false);
+		    confirmation.setEnabled(false);
+		    confirmation.setSelected(false);
+		}
+	    }
+	    else {
+		confirmation.setEnabled(false);
+		details.setText(Dictionary.get("DeleteCollectionPrompt.No_Collection"));
+	    }
+	}
+    }
+
+    /** A check box listener so we can tell if the user has confirmed the deletion */
+    private class ConfirmationCheckBoxListener
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so we can be informed when the button is actioned.
+	 * @param event An <strong>ActionEvent</strong> containing all the relevant information garnered from the event itself.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    // OK button is only enabled if the confirmation check box is ticked
+	    ok_button.setEnabled(confirmation.isSelected());
+	}
+    }
+
+    /** The OK button listener implementation. */
+    private class OKButtonListener 
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so we can be informed when the button is actioned.
+	 * @param event An <strong>ActionEvent</strong> containing all the relevant information garnered from the event itself.
+	 * @see org.greenstone.gatherer.Configuration
+	 * @see org.greenstone.gatherer.Gatherer
+	 * @see org.greenstone.gatherer.util.Utility
+	 */
+	public void actionPerformed(ActionEvent event)
+	{
+	    // Delete the selected collection.
+	    if (Gatherer.c_man.deleteCollection(collection.getShortName())) {
+		// Refresh the collections shown in the workspace tree
+		Gatherer.g_man.refreshWorkspaceTree(WorkspaceTree.LIBRARY_CONTENTS_CHANGED);
+
+		if (collection.getShortName().equals(CollectionManager.getLoadedCollectionName())) {
+		    current_coll_deleted = true;
+		}
+		list_model.removeElement(collection);
+
+		if (!Configuration.fedora_info.isActive()) { // for fedora collections, delete in background
+		    resultPrompt(true);
+		}
+		details.setText(Dictionary.get("DeleteCollectionPrompt.No_Collection"));
+		confirmation.setEnabled(false);
+		confirmation.setSelected(false);
+		ok_button.setEnabled(false);
+		collection = null;
+	    }
+	    else {
+		resultPrompt(false);
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DesignPane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DesignPane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DesignPane.java	(revision 31635)
@@ -0,0 +1,77 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.cdm.Control;
+
+import org.greenstone.gatherer.Configuration;
+
+public class DesignPane 
+    extends BaseConfigPane {
+
+    /** The constructor. */
+    public DesignPane() {
+	super();
+	if (Configuration.fedora_info.isActive()) {
+	    contents = new String [] { "CDM.GUI.Plugins" };
+	}
+	else {
+	    contents = new String [] { "CDM.GUI.Plugins", "CDM.GUI.Indexes", "CDM.GUI.Subcollections",  "CDM.GUI.Classifiers" };
+	}
+	
+    }
+
+    protected Control getSubControls(String type) {
+	if(type.equals("CDM.GUI.Plugins")) {
+	    return  CollectionDesignManager.plugin_manager.getControls();
+	}
+	if(type.equals("CDM.GUI.Indexes")) {
+	    return CollectionDesignManager.index_manager.getControls();
+	}
+	if(type.equals("CDM.GUI.Subcollections")) {
+	    return CollectionDesignManager.subcollection_manager.getControls();
+	}
+	if(type.equals("CDM.GUI.Classifiers")) {
+	    return CollectionDesignManager.classifier_manager.getControls();
+	}
+
+	return null;
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DesignPaneHeader.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DesignPaneHeader.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DesignPaneHeader.java	(revision 31635)
@@ -0,0 +1,82 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Katherine Don, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.gui.HelpFrame;
+import org.greenstone.gatherer.gui.MenuBar;
+
+/** creates a header pane containing the title of the screen, and a help button which links to the appropriate help page */
+public class DesignPaneHeader
+    extends JPanel
+{
+    protected String help_key_name;
+
+
+    public DesignPaneHeader(String label_key, String help_key)
+    {
+        this.setComponentOrientation(Dictionary.getOrientation());
+	JLabel title_label = new JLabel("<HTML><STRONG><FONT FACE={FONT} COLOR=Black SIZE=5>" + Dictionary.get(label_key) + "</FONT></STRONG></HTML>");
+        title_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel title_pane = new JPanel();
+	title_pane.setBorder(BorderFactory.createEmptyBorder(0,0,0,5));
+	title_pane.setLayout(new BorderLayout());
+	title_pane.add(title_label, BorderLayout.CENTER);
+	title_pane.add(new JSeparator(), BorderLayout.SOUTH);
+        title_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	GLIButton help_button = new GLIButton(Dictionary.get("CDM.HelpButton"), Dictionary.get("CDM.HelpButton_Tooltip"));
+	this.help_key_name = help_key;
+	help_button.setIcon(MenuBar.HELP_ICON);
+	help_button.addActionListener(new ActionListener()
+	    {
+		public void actionPerformed(ActionEvent event) {
+		    HelpFrame.setView(help_key_name);
+		}
+	    });
+
+	setLayout(new BorderLayout());
+	add(title_pane, BorderLayout.CENTER);
+	add(help_button, BorderLayout.LINE_END);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DownloadPane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DownloadPane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/DownloadPane.java	(revision 31635)
@@ -0,0 +1,799 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.file.WorkspaceTree;
+import org.greenstone.gatherer.greenstone.LocalGreenstone;
+import org.greenstone.gatherer.util.SafeProcess;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.download.Download;
+import org.greenstone.gatherer.download.DownloadScrollPane;
+import org.greenstone.gatherer.download.ServerInfoDialog;
+import org.greenstone.gatherer.util.XMLTools;
+import org.greenstone.gatherer.cdm.*;
+import org.greenstone.gatherer.gui.*;
+import org.w3c.dom.*;
+import org.xml.sax.*;
+import org.greenstone.gatherer.GAuthenticator;
+
+/**
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.1
+ */
+public class DownloadPane
+    extends JPanel {
+
+    static final private Dimension LABEL_SIZE = new Dimension(225, 25);
+    static final private Dimension TREE_SIZE = new Dimension(150, 500);
+    //static final private String CONTENTS[] = { "DOWNLOAD.MODE.WebDownload", "DOWNLOAD.MODE.MediaWikiDownload", "DOWNLOAD.MODE.OAIDownload", "DOWNLOAD.MODE.ZDownload" , "DOWNLOAD.MODE.SRWDownload"};
+    private String CONTENTS[] = null;
+
+    private boolean download_button_enabled = false;
+    private boolean ready = false;
+
+    private JPanel options_pane;
+    // TODO should use Vector to store all loaded downloads!!
+
+    private DesignTree tree;
+    private HashMap download_map;
+    private ServerInfoDialog server_info;
+    private JScrollPane list_scroll;
+    private DownloadScrollPane getter;
+    private String mode = null;
+    private TreePath previous_path;
+    private String proxy_url = "";
+	    
+    /** Main System code */
+    public DownloadPane() {
+	super();
+        JScrollPane scrol_tmp;
+        this.setComponentOrientation(Dictionary.getOrientation());
+	// TODO: Download the WDownload and the download panel fixed!!
+	getter = new DownloadScrollPane();
+        getter.start();
+	list_scroll = getter.getDownloadJobList();      
+        list_scroll.setComponentOrientation(Dictionary.getOrientation());
+
+	// TODO should use Vector to store all loaded downloads!!
+	String lang = Configuration.getLanguage();
+	download_map = new HashMap();
+
+	// run downloadinfo.pl -describeall, load the downloaders into the download_map, 
+	// and get back their list of names, which are of the form "<downloader>Download". 
+	// Store these names in the CONTENTS[] array as "DOWNLOAD.MODE.<downloader>Download", 
+	// with z3950 as a minor exception: DOWNLOAD.MODE.ZDownload.
+	ArrayList<String> downloaderNamesList = loadDownloadersInfo(lang);	
+	int size = downloaderNamesList.size();
+	CONTENTS = new String[size];
+	for(int i = 0; i < size; i++) {
+	    String downloadName = downloaderNamesList.get(i); // e.g. "WebDownload"	    
+	    CONTENTS[i] = "DOWNLOAD.MODE."+downloadName.replace("3950", ""); // A special case is Z3950Download,
+	                                 // which has to be stored in CONTENTS array as DOWNLOAD.MODE.ZDownload
+	}
+
+	// Creation
+	tree = new DesignTree();
+        tree.setComponentOrientation(Dictionary.getOrientation());
+	options_pane = new JPanel();
+        options_pane.setComponentOrientation(Dictionary.getOrientation());
+	
+	JButton clear_cache_button = new GLIButton(Dictionary.get("Mirroring.ClearCache"), Dictionary.get("Mirroring.ClearCache_Tooltip"));
+	clear_cache_button.setEnabled(true);
+	clear_cache_button.setMnemonic(KeyEvent.VK_C);
+
+	JButton download_button = new GLIButton(Dictionary.get("Mirroring.Download"), Dictionary.get("Mirroring.Download_Tooltip"));
+	download_button.setEnabled(true);
+	download_button.setMnemonic(KeyEvent.VK_D);
+
+	JButton information_button =  new GLIButton(Dictionary.get("Download.ServerInformation"), Dictionary.get("Download.ServerInformation_Tooltip"));
+	information_button.setEnabled(true);
+	information_button.setMnemonic(KeyEvent.VK_S);
+
+
+       	JButton preferences_button =  new GLIButton(Dictionary.get("Mirroring.Preferences"), Dictionary.get("Mirroring.Preferences_Tooltip"));
+	preferences_button.setEnabled(true);
+	preferences_button.setMnemonic(KeyEvent.VK_P);
+
+	// Connect
+	clear_cache_button.addActionListener(new ClearCacheListener());
+	download_button.addActionListener(new DownloadButtonListener());
+	preferences_button.addActionListener(new PreferencesButtonActionListener());
+	information_button.addActionListener(new InformationButtonActionListener());
+	tree.addTreeSelectionListener(new TreeListener());
+
+	// Add to Panel
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	button_pane.setLayout(new GridLayout(1,4)); // GridLayout so button pane resizes with window-width
+	button_pane.setBorder(BorderFactory.createEtchedBorder());
+	button_pane.add(clear_cache_button);
+	button_pane.add(download_button);
+	button_pane.add(information_button);
+	button_pane.add(preferences_button);
+
+	JPanel tree_pane = new JPanel();
+        tree_pane.setComponentOrientation(Dictionary.getOrientation());
+	tree_pane.setLayout(new BorderLayout());
+        scrol_tmp = new JScrollPane(tree);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+       	tree_pane.add(scrol_tmp, BorderLayout.CENTER);
+	tree_pane.setPreferredSize(TREE_SIZE);
+ 
+
+	Color colour_two = Configuration.getColor("coloring.collection_tree_background", false);
+	options_pane.setBackground(colour_two);
+	options_pane.setBorder(BorderFactory.createEtchedBorder());
+
+
+	JScrollPane options_scroll_pane = new JScrollPane(options_pane);
+        options_scroll_pane.setComponentOrientation(Dictionary.getOrientation());
+	JSplitPane mode_pane = new JSplitPane();
+        mode_pane.setComponentOrientation(Dictionary.getOrientation());
+	mode_pane.setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
+        if (Dictionary.getOrientation().isLeftToRight()){
+            mode_pane.add(tree_pane,JSplitPane.LEFT);
+            mode_pane.add(options_scroll_pane,JSplitPane.RIGHT); 
+            mode_pane.setDividerLocation(TREE_SIZE.width);
+        }else{
+            mode_pane.add(tree_pane,JSplitPane.RIGHT);
+            mode_pane.add(options_scroll_pane,JSplitPane.LEFT);
+            mode_pane.setDividerLocation(1-TREE_SIZE.width);
+        }
+		
+    
+	JPanel edit_pane = new JPanel();
+        edit_pane.setComponentOrientation(Dictionary.getOrientation());
+	edit_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(2,0,0,0), BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder("Download Setting"), BorderFactory.createEmptyBorder(2,2,2,2))));
+	edit_pane.setLayout(new BorderLayout());
+	edit_pane.add(mode_pane,BorderLayout.CENTER);
+	edit_pane.add(button_pane,BorderLayout.PAGE_END);
+	
+	// Add to "this"
+	setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	setLayout(new GridLayout(2,1));
+	add(edit_pane);
+	add(list_scroll);
+
+	//set the mode to the first downloader in the list
+	mode = convertCONTENTStoMode(CONTENTS[0]); // e.g. Web
+	generateOptions(options_pane,(Download)download_map.get(mode));
+	previous_path = tree.getSelectionPath();
+    }
+
+    /** System Utilities */
+    public void modeChanged(int gli_mode) {
+	// do nothing at this stage - should we be renewing download options??
+    }
+
+    private void addHeader(String name, Color color, JPanel target_pane) {
+	JPanel header = new JPanel();
+        header.setComponentOrientation(Dictionary.getOrientation());
+	header.setBackground(color);
+	JPanel inner_pane = new JPanel();
+        inner_pane.setComponentOrientation(Dictionary.getOrientation());
+	inner_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(5,5,5,5), BorderFactory.createRaisedBevelBorder()));
+	inner_pane.setBackground(color);
+	JLabel header_label = new JLabel("<html><strong>" + name + "</strong></html>");
+        header_label.setComponentOrientation(Dictionary.getOrientation());
+	header_label.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+	header_label.setHorizontalAlignment(JLabel.CENTER);
+	header_label.setOpaque(true);
+
+	// Layout
+	inner_pane.setLayout(new BorderLayout());
+	inner_pane.add(header_label, BorderLayout.CENTER);
+
+	header.setLayout(new BorderLayout());
+	header.add(inner_pane, BorderLayout.CENTER);
+	target_pane.add(header);
+    }
+
+    /** Supporting Functions */
+    private ArrayList<String> loadDownloadersInfo(String lang) {
+	Document document = null;
+	
+	try {
+	    if (Gatherer.isGsdlRemote) {
+		String output = Gatherer.remoteGreenstoneServer.getScriptOptions("downloadinfo.pl", "&describeall");
+		Reader reader = new StringReader(output);
+		document = XMLTools.parseXML(reader);
+	    }
+	    else {
+		ArrayList args_list = new ArrayList();
+		String args[] = null;
+		if(Configuration.perl_path != null) {
+		    args_list.add(Configuration.perl_path);
+		} else if(Utility.isWindows()) {
+		    args_list.add("Perl.exe");
+		} else {
+		    args_list.add("perl");
+		}
+		args_list.add("-S");
+		args_list.add(LocalGreenstone.getBinScriptDirectoryPath()+"downloadinfo.pl");
+		args_list.add("-describeall");
+		args_list.add("-xml");
+		args_list.add("-language");
+		args_list.add(lang);		
+		
+		// Create the process.
+		args = (String []) args_list.toArray(new String[0]);
+
+		SafeProcess process = new SafeProcess(args);
+		DebugStream.println("Getting Download Info: "+args_list);
+
+		// run the SafeProcess
+		int exitVal = process.runProcess();
+		if(exitVal != 0) {
+		    throw new Exception("*** Error running Download Info process, process exited with: "
+					+ exitVal);
+		}		
+		
+		// get the result and process it.
+		// This time we expect XML to have come out of the process std error stream.
+		String errStreamOutput = process.getStdError();
+		///System.err.println("*********\nDownload Pane data, got:\n" + errStreamOutput + "\n**********\n");
+		StringReader xmlStrReader = new StringReader(errStreamOutput);
+		document = XMLTools.parseXML(xmlStrReader);
+		xmlStrReader.close();
+		
+	    }
+
+            
+	}
+	catch (Exception error) {
+	    System.err.println("Failed when trying to parse downloadinfo.pl -describeall");
+	    error.printStackTrace();
+	}
+
+	if(document != null) {
+	    return parseXML(document.getDocumentElement());
+	}
+
+	return null;
+    }
+
+    private ArrayList<String> parseXML(Node root) {
+	ArrayList<String> downloaders = null;
+	Element downloadList = (Element)root;
+	int length = -1;
+	if(downloadList.hasAttribute("length")) {
+	    length = Integer.parseInt(downloadList.getAttribute("length"));
+	    downloaders = new ArrayList<String>(length);		
+	    
+	    for (Node node = downloadList.getFirstChild(); node != null; node = node.getNextSibling()) {
+		// goes through each <DownloadInfo> of describeAll
+		
+		String download_name = null;
+		
+		for(Node infoNode = node.getFirstChild(); 
+		    infoNode != null; infoNode = infoNode.getNextSibling()) {
+		    
+		    String node_name = infoNode.getNodeName();
+		    if(node_name.equalsIgnoreCase("Name")) { // <Name>WebDownload</Name>
+			download_name = XMLTools.getValue(infoNode); // e.g. WebDownload		
+		    }
+		    
+		    // At this top level of <DownloadInfo> elements,
+		    // skip all the downloaders that are Abstract, as these are pure superclasses		
+		    else if(node_name.equalsIgnoreCase("Abstract")) {
+			String isAbstract = XMLTools.getValue(infoNode);
+			
+			if(isAbstract.equalsIgnoreCase("no") && download_name != null) {
+			    downloaders.add(download_name);
+			    Download downloader = parseDownloadInfoXML(node); // parse the <DownloadInfo> node properly
+			    // now embedded references to abstract superclasses (embedded <DownloadInfo> nodes)
+			    // will be handled
+			    
+			    String shortName = download_name.replace("Download", ""); // e.g. "Web"
+			    download_map.put(shortName, downloader);
+			}
+		    }
+		}
+	    }
+	}
+
+	return downloaders;
+    }
+
+    private Download parseDownloadInfoXML(Node root) {
+
+	Download download = new Download();
+	String node_name = null;
+	for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) {
+	    node_name = node.getNodeName();
+	    if(node_name.equalsIgnoreCase("Name")) {
+		String name = XMLTools.getValue(node);
+		download.setName(name);
+	    }
+	    else if (node_name.equalsIgnoreCase("Desc")) {
+		download.setDescription(XMLTools.getValue(node));
+	    }
+	    else if (node_name.equalsIgnoreCase("Abstract")) {
+		download.setIsAbstract(XMLTools.getValue(node).equalsIgnoreCase(StaticStrings.YES_STR));
+	    }
+	    else if(node_name.equalsIgnoreCase("Arguments")) {
+		for(Node arg = node.getFirstChild(); arg != null; arg = arg.getNextSibling()) {
+		    node_name = arg.getNodeName();
+                     if(node_name.equalsIgnoreCase("Option")) {
+			Argument argument = new Argument((Element)arg);
+			argument.parseXML((Element)arg);
+			argument.setValue(argument.getDefaultValue());
+			download.addArgument(argument);
+		     }
+		   
+		}
+	    }
+            else if(node_name.equalsIgnoreCase("DownloadInfo")) {
+                Download super_download = parseDownloadInfoXML(node);
+		download.setSuper(super_download);
+	    }
+	}
+
+	if(download.getName() != null) {
+	    return download;
+	}
+	return null;
+    }
+
+    /** Update the previous setup */
+    private boolean updateArguments(boolean checkRequired)
+    {
+	boolean cont = true;
+	for(int i = 0; i < options_pane.getComponentCount(); i++) {
+	   
+	    Component component = options_pane.getComponent(i);
+	    if(component instanceof ArgumentControl) {
+		cont = cont && ((ArgumentControl)component).updateArgument(checkRequired);
+	    }
+	}
+
+	if(cont){return true; }
+
+	return false;
+    }
+    
+    /** Generate Controls for Options */
+    /* at some stage we should think about which options should be shown for
+     * different modes. Currently, always show all options (unless hidden)*/
+    private void generateOptions(JPanel options_pane, ArgumentContainer data) {
+	options_pane.removeAll();
+	/** Create the current option panel */
+
+	ArrayList arguments = data.getArguments(true, false);
+      	int mode = Configuration.getMode();
+        ArrayList added_arguments = new ArrayList();
+     
+	for(int i = 0; i < arguments.size(); i++) {
+	    Argument argument = (Argument) arguments.get(i);
+
+	    if (argument.isHiddenGLI()) continue;
+	    ArgumentControl argument_control = new ArgumentControl(argument,false,null);
+	    added_arguments.add(argument_control);
+	}
+	
+
+       	options_pane.setLayout(new GridLayout(added_arguments.size(),1));
+        for(int i = 0; i <  added_arguments.size(); i++) { 
+	    options_pane.add((ArgumentControl)added_arguments.get(i));
+	    
+	}
+    }
+
+    /** Behaviour Functions */
+    public void afterDisplay() {
+	ready = true;        
+    }
+
+
+    public void gainFocus() {
+	if(!ready) {
+	    return;
+	}
+
+	// It is also a good time to determine if the download should be enabled - ie if its allowed to be enabled and a valid URL is present in the field.
+	download_button_enabled = true;
+	//download_button.setEnabled(download_button_enabled);
+    }
+
+
+
+    public void refresh(int refresh_reason, boolean ready)
+    {
+    }
+
+    /** Private classes */
+    /** This tree provides a 'table of contents' for the various components of the design process (collection configuration in more technical terms). */
+    private class DesignTree extends JTree {
+
+	private DesignNode root = null;
+	/** Constructor. Automatically generates all of the nodes, in the order of CONTENTS. */
+	public DesignTree() {
+	    super();
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    resetModel(Configuration.getMode());
+	    expandRow(0);
+	    setRootVisible(false);
+	    setSelectionRow(0);
+	}
+
+	/** Reset the model used by the design page contents tree. This is necessary to hide the partitions entry when in lower detail modes
+	 * @param mode the current detail mode as an int
+	 */
+	public void resetModel(int mode) {
+	    root = new DesignNode("DOWNLOAD.MODE.Root");
+	    // Now add the design categories.
+	    for(int i = 0; i < CONTENTS.length; i++) {
+		root.add(new DesignNode(CONTENTS[i]));
+	    }
+	    this.setModel(new DefaultTreeModel(root));
+	    updateUI();
+	}
+	/** Set the current view to the one specified.
+	 * @param type the name of the desired view as a String
+	 */
+	public void setSelectedView(String type) {
+	    type = Dictionary.get(type);
+	    for(int i = 0; i < root.getChildCount(); i++) {
+		DesignNode child = (DesignNode) root.getChildAt(i);
+		if(child.toString().equals(type)) {
+		    TreePath path = new TreePath(child.getPath());
+		    setSelectionPath(path);
+		}
+	    }
+	}
+    }
+
+    /** A tree node that retains a reference to one of the possible design sub-views relating to the different sub-managers. */
+    private class DesignNode extends DefaultMutableTreeNode {
+	/** Constructor.
+	 * @param object The <strong>Object</strong> assigned to this node.
+	 */
+	public DesignNode(String object) {
+	    super(object);
+	}
+	/** Retrieve a textual representation of the object.
+	 * @return a String
+	 */
+	public String toString() {
+	    // return Dictionary.get("CDM.GUI." + (String)getUserObject());
+	    return Dictionary.get((String) getUserObject());
+	}
+    }
+
+    /** Listens for selection changes in the 'contents' tree, and switches to the appropriate view. */
+    private class TreeListener
+	implements TreeSelectionListener {
+	/** Called whenever the selection changes, we must update the view so it matches the node selected.
+	 * @param event A <strong>TreeSelectionEvent</strong> containing more information about the tree selection.
+	 * @see org.greenstone.gatherer.cdm.ClassifierManager
+	 * @see org.greenstone.gatherer.cdm.CollectionDesignManager
+	 * @see org.greenstone.gatherer.cdm.CollectionMetaManager
+	 * @see org.greenstone.gatherer.cdm.FormatManager
+	 * @see org.greenstone.gatherer.cdm.LanguageManager
+	 * @see org.greenstone.gatherer.cdm.MetadataSetView
+	 * @see org.greenstone.gatherer.cdm.SubcollectionManager
+	 * @see org.greenstone.gatherer.cdm.TranslationView
+	 * @see org.greenstone.gatherer.cdm.PlugInManager
+	 */
+	public void valueChanged(TreeSelectionEvent event) {
+	    if(!tree.isSelectionEmpty()) {
+		TreePath path = tree.getSelectionPath();
+
+		DesignNode node = (DesignNode)path.getLastPathComponent();
+		String type = (String)node.getUserObject();
+		Gatherer.g_man.wait(true);
+
+		// type has the value DOWNLOAD.MODE.<downloader>Download,
+		// mode should then be of the form <downloader>
+		mode = convertCONTENTStoMode(type);
+		generateOptions(options_pane,(Download)download_map.get(mode));
+
+		tree.setSelectionPath(path);
+		previous_path = path;
+		repaint();
+
+		Gatherer.g_man.wait(false);
+	    }
+	}
+    }
+
+    private String convertCONTENTStoMode(String content) {
+	return content.replace("DOWNLOAD.MODE.", "").replace("ZDownload", "Z3950").replace("Download", "");
+    }
+
+    private class ClearCacheListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    // Retrieve the cache folder and delete it.
+	    Utility.delete(Utility.getCacheDir());
+	    // ...and refresh the node in the workspace tree to show it's all gone
+	    Gatherer.g_man.refreshWorkspaceTree(WorkspaceTree.DOWNLOADED_FILES_CHANGED);
+	}
+    }
+
+    private class DownloadButtonListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+
+	    if(checkURL(true) && checkProxy() == true) {
+		
+		// Proxy settings are now set. Check that the url is not a redirect, else get
+		// redirect url (we do this step in order to avoid some unintuitive behaviour from wget)
+		Download current_download = (Download)download_map.get(mode);
+		Argument arg_url = current_download.getArgument("url");
+		if(arg_url != null) { // it's null for z3950 and possibly for other downloaders
+		    String url_str = arg_url.getValue();
+		    String redirect_url_str = getRedirectURL(url_str);
+		    
+		    // only update the Argument and its GUI ArgumentControl if the URL
+		    // has in fact changed
+		    if(!url_str.equals(redirect_url_str)) {
+			arg_url.setValue(redirect_url_str);
+			updateArgument(arg_url, redirect_url_str);
+		    }
+		}
+		getter.newDownloadJob((Download)download_map.get(mode) ,mode,proxy_url);
+	    }
+	}
+    }
+
+    /**
+     * The Java code here will retrieve the page at the given url. If the response code is 
+     * a redirect, it will get the redirect url so that wget may be called with the proper url. 
+     * This preprocessing of the URL is necessary because:
+     * Wget does not behave the way the browser does when faced with urls of the form
+     * http://www.englishhistory.net/tudor/citizens and if that page does not exist.
+     * The directory listing with a slash at the end (http://www.englishhistory.net/tudor/citizens/)
+     * does exist, however. In order to prevent wget from assuming that the root URL
+     * to traverse is http://www.englishhistory.net/tudor/ instead of the intended
+     * http://www.englishhistory.net/tudor/citizens/, we need give wget the redirect location
+     * that's returned when we initially make a request for http://www.englishhistory.net/tudor/citizens
+     * The proper url is sent back in the Location header, allowing us to bypass wget's 
+     * unexpected behaviour.
+     * This method ensures that urls like http://www.nzdl.org/niupepa also continue to work:
+     * there is no http://www.nzdl.org/niupepa/ page, because this url actually redirects to an 
+     * entirely different URL.
+     * @return the redirect url for the given url if any redirection is involved, or the
+     * url_str.
+     */
+    private String getRedirectURL(String url_str) {
+	HttpURLConnection connection = null;
+	if(url_str.startsWith("http:")) { // only test http urls
+	    try {
+		URL url = new URL(url_str); 
+		connection = (HttpURLConnection)url.openConnection(); //new HttpURLConnection(url);
+		// don't let it automatically follow redirects, since we want to
+		// find out whether we are dealing with redirects in the first place
+		connection.setInstanceFollowRedirects(false);
+		
+		// now check for whether we get a redirect response
+		// HTTP Codes 3xx are redirects, http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+		int responseCode = connection.getResponseCode();
+		if(responseCode >= 300 && responseCode < 400) { 
+		    String responseMsg = connection.getResponseMessage();
+
+		    // Get the Location header since this specifies the new location of the resource
+		    String location = connection.getHeaderField("Location");
+		    
+		    // this becomes the url that wget should download from
+		    url_str = location.trim();
+		}
+		
+		connection.disconnect();
+	    } catch(Exception e) {
+		if(connection != null) {
+		    connection.disconnect();
+		}
+		System.err.println("Checking redirection. Tried to connect to "
+				   + url_str + ",\nbut got exception: " + e);
+	    }	    
+	} 
+
+	return url_str;
+    }
+
+    
+    /** For a string-based Argument whose value has changed, this method
+     * updates the GUI ArgumentControl's value correspondingly. */
+    private void updateArgument(Argument arg, String value) {
+	for(int i = 0; i < options_pane.getComponentCount(); i++) {
+	    Component component = options_pane.getComponent(i);
+	    if(component instanceof ArgumentControl) {
+		ArgumentControl control = (ArgumentControl)component;
+		if(control.getArgument() == arg) {
+		    control.setValue(value);
+		    control.repaint();
+		}
+	    }
+	}
+    }
+
+    private boolean checkURL(boolean checkRequired){
+	
+	if (!updateArguments(checkRequired)){
+	    return false;
+	}
+
+	Download current_download = (Download)download_map.get(mode);
+	Argument arg_url = current_download.getArgument("url");
+	
+        if (arg_url == null) return true;
+ 
+	String url_str = arg_url.getValue();
+	URL url = null;
+	try {
+	    url = new URL(url_str);
+	}
+	catch(MalformedURLException error) {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("Mirroring.Invalid_URL"), Dictionary.get("Mirroring.Invalid_URL_Title"), JOptionPane.ERROR_MESSAGE);
+	    return false;
+	}
+
+	return true;
+    }
+
+    
+    private boolean checkProxy(){
+	 
+	proxy_url = null;
+
+	Download current_download = (Download)download_map.get(mode);
+	
+        Argument arg = current_download.getArgument("proxy_on");
+  
+	if (arg == null) return true;
+	
+	// Determine if we have to use a proxy.
+	if(Configuration.get("general.use_proxy", true)) {
+          
+	    String proxy_host = Configuration.getString("general.proxy_host", true);
+	    String proxy_port = Configuration.getString("general.proxy_port", true);
+	    // Find out whether the user has already authenticated themselves
+	    String user_pass = "";
+	    String address = proxy_host + ":" + proxy_port;
+	    
+            int count = 0;
+	    // Only for wget, need to avoid a second automatic authentication popup (first asks
+	    // the proxy authentication for wget, and the second will ask the same for the realm)
+	    // Once the authentication has been reused, it will set the GAuthenticator state back to REGULAR
+	    GAuthenticator.setMode(GAuthenticator.DOWNLOAD);
+	    while(count < 3 && (user_pass = (String) GAuthenticator.authentications.get(address)) == null) {
+		Authenticator.requestPasswordAuthentication(proxy_host, null, Integer.parseInt(proxy_port), "http://", Dictionary.get("WGet.Prompt"), "HTTP");
+		count++;
+	    }
+	    if(count >= 3) {
+		return false;
+	    }
+
+	    if(user_pass.indexOf("@") != -1) {
+			
+		// Write the use proxy command - we don't do this anymore, instead we set environment variables - hopefully these can't be spied on like the following can (using ps) - actually the environment stuff didn't work for windows, so lets go back to this
+		if (Utility.isWindows()) {
+		     
+		   arg.setValue("true");
+		   arg.setAssigned(true);
+
+		   arg = current_download.getArgument("proxy_host");
+		   arg.setValue(proxy_host);
+		   arg.setAssigned(true);
+
+		   arg = current_download.getArgument("proxy_port");
+		   arg.setValue(proxy_port);
+		   arg.setAssigned(true);
+
+		   arg = current_download.getArgument("user_name");
+		   arg.setValue(user_pass.substring(0, user_pass.indexOf("@")));
+		   arg.setAssigned(true);
+
+		   arg = current_download.getArgument("user_password");
+		   arg.setValue(user_pass.substring(user_pass.indexOf("@") + 1));
+		   arg.setAssigned(true);
+		  }
+	       else{
+ 		    String user_name = user_pass.substring(0, user_pass.indexOf("@"));
+ 		    String user_pwd = user_pass.substring(user_pass.indexOf("@") + 1);
+ 		    proxy_url = user_name+":"+user_pwd+"@"+proxy_host+":"+proxy_port+"/";
+		   
+ 	       }
+		
+	       return true;
+	    }
+	    else{
+		return false;
+	    }
+
+	}
+	
+	return true;
+    }
+
+    /*
+    private class PreferencesButtonActionListener
+    implements ActionListener {
+    public void actionPerformed(ActionEvent event) {
+    new Preferences(Preferences.CONNECTION_PREFS);
+    }
+    }*/
+    
+    private class InformationButtonActionListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    //turn off the check for find argument
+	    Download current_download = (Download)download_map.get(mode);
+
+            if (!checkProxy() || !checkURL(false) )return;
+
+
+	    if(server_info != null) {
+		server_info.dispose();
+	    }
+
+	   
+	    Argument arg_url = current_download.getArgument("url");
+	    String str_url = "";
+
+	    if( arg_url!= null && arg_url.isAssigned()) {
+		str_url = arg_url.getValue();
+	    }
+
+            
+ 	    server_info = new ServerInfoDialog(str_url ,proxy_url, mode,(Download)download_map.get(mode));
+ 	    
+	}
+    }
+
+     private class PreferencesButtonActionListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    new Preferences(Preferences.CONNECTION_PREFS);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/EditorDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/EditorDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/EditorDialog.java	(revision 31635)
@@ -0,0 +1,156 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+
+/** A class that extends a JDialog into a editor for editing large block of text for the metadata value.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+final public class EditorDialog
+    extends ModalDialog 
+    implements ActionListener {
+   /** Is this dialog editable? */
+   private boolean editable = true;
+   /** The cancel, and I don't want the text I've typed, button. */
+    private JButton cancel = null;
+    /** The ok, I'll save what I've just typed in, button. */
+    private JButton ok = null;
+    /** The area in which we type. */
+    private JTextArea text = null;
+    /** And what result should be passed back to our caller. */
+    private String result = null;
+    /** The size of the edit pop-up. */
+    final static private Dimension SIZE = new Dimension(400, 300);
+
+    /** Constructor */
+    public EditorDialog() {
+	super(Gatherer.g_man);
+        this.setComponentOrientation(Dictionary.getOrientation());
+    }
+
+    /** Any implementation of ActionListener must include this method so we can be informed when an action has been performed on one of our target controls. In this case we generate a pop-up window to edit in.
+     * @param event An <strong>ActionEvent</strong> containing information about the event.
+     */
+    public void actionPerformed(ActionEvent event) {
+	if (event.getSource() == ok) {
+	    result = text.getText();
+	}
+	dispose();
+    }
+
+    /** Method to display the editing box on screen.
+     * @param value The initial text to be displayed in the editing area, as a <strong>String</strong>.
+     * @return The new value for the metadata value as a <strong>String</strong> or <i>null</i> if the user has pressed cancel.
+     */
+    public String display(String value) {
+	setModal(true);
+	setSize(SIZE);
+	setJMenuBar(new SimpleMenuBar("theenrichview"));
+	if (editable) {
+	    setTitle(Dictionary.get("General.Edit"));
+	} else {
+	    setTitle(Dictionary.get("General.View"));
+	}
+	// Create
+	text = new JTextArea(value);
+        text.setComponentOrientation(Dictionary.getOrientation());
+	text.setCaretPosition(value.length());
+	text.setEditable(editable);
+	text.setLineWrap(true);
+	text.setWrapStyleWord(false);
+	if (editable) {
+	    text.setToolTipText(Dictionary.get("EnrichPane.Value_Field_Tooltip"));
+	} else {
+	    text.setToolTipText(Dictionary.get("EnrichPane.Value_Field_Tooltip_Uneditable"));
+	}
+
+	cancel = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Pure_Cancel_Tooltip"));
+	
+	ok = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+	
+
+	// Listeners
+	cancel.addActionListener(this);
+	ok.addActionListener(this);
+
+	// Layout
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	button_pane.setLayout(new GridLayout(1,2));
+	if(editable) {
+	   button_pane.add(ok);
+	   button_pane.add(cancel);
+	}
+	else {
+	   button_pane.add(new JPanel());
+	   button_pane.add(ok);
+	}
+		  
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setLayout(new BorderLayout());
+        JScrollPane scrol_tmp;
+        scrol_tmp = new JScrollPane(text);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.add(scrol_tmp, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+		  
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(true);
+	return result;
+    }
+
+    /** Specify if this text dialog should be editable or readonly
+     * @param editable true to allow editing, false otherwise
+     */
+    public void setEditable(boolean editable) {
+      if(text == null) {
+	 this.editable = editable;
+      }
+      else {
+	 text.setEditable(editable);
+      }
+   }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/EmailField.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/EmailField.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/EmailField.java	(revision 31635)
@@ -0,0 +1,69 @@
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.util.regex.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+import org.greenstone.gatherer.Dictionary;
+
+public class EmailField
+    extends JTextField 
+    implements DocumentListener {
+
+    static final public Pattern EMAIL_PATTERN = Pattern.compile("([^()\\-<> @,;:\"][^()<> @,;:\"]*|\"[^()<>@,;:\"]+\")@([A-Za-z](-*[A-Za-z0-9])*(\\.[A-Za-z](-*[A-Za-z0-9])*)*)");
+
+    private boolean invalid = false;
+    private Color background;
+    private Color invalid_background;
+
+    public EmailField(Color invalid_background) {
+	super();
+        this.setComponentOrientation(Dictionary.getOrientation());
+	this.invalid_background = invalid_background;
+    }
+
+    public EmailField(String email, Color invalid_background) {
+	super(email);
+	this.invalid_background = invalid_background;
+    }
+
+    /** Gives notification that an attribute or set of attributes changed.
+     * @param e
+     */
+    public void changedUpdate(DocumentEvent e) {
+	validateEmail();
+    }
+          
+    /** Gives notification that there was an insert into the document.
+     * @param e
+     */
+    public void insertUpdate(DocumentEvent e) {
+	validateEmail();
+    }
+          
+    /** Gives notification that a portion of the document has been removed. 
+     * @param e
+     */
+    public void removeUpdate(DocumentEvent e) {
+	validateEmail();
+    }
+
+    private void validateEmail() {
+	Matcher m = EMAIL_PATTERN.matcher(getText());
+	if(m.matches()) {
+	    // It was invalid, but now its valid again
+	    if(invalid) {
+		setBackground(background);
+		invalid = false;
+	    }
+	    // Otherwise nothings changed so why do anything
+	}
+	else {
+	    background = getBackground();
+	    setBackground(invalid_background);
+	    invalid = true;
+	}
+	m = null;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/EnrichPane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/EnrichPane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/EnrichPane.java	(revision 31635)
@@ -0,0 +1,430 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ * Based on code by John Thompson
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.gui;
+
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionTree;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.gui.tree.DragTree;
+import org.greenstone.gatherer.metadata.FilenameEncoding;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataValue;
+import org.greenstone.gatherer.metadata.MetadataValueTableEntry;
+import org.greenstone.gatherer.metadata.MetadataValueTreeNode;
+import org.greenstone.gatherer.metadata.MetadataXMLFileManager;
+
+
+/** Provides a view of controls for the editing of metadata.
+ */
+public class EnrichPane
+    extends JPanel
+    implements TreeSelectionListener
+{
+    static private Dimension MINIMUM_SIZE = new Dimension(100, 100);
+    static private Dimension COLLECTION_TREE_SIZE = new Dimension(250, 500);
+
+    private boolean has_focus = false;
+    /** The collection tree. */
+    private CollectionTree collection_tree = null;
+    /** The currently reported selection. */
+    private CollectionTreeNode[] file_nodes = null;
+    /** Used to dynamically filter the collection tree. */
+    private Filter collection_filter = null;
+    /** The label at the top of the collection tree, which shows the collection name. */
+    private JLabel collection_label;
+    /** The panel that contains the collection tree. */
+    private JPanel collection_pane = null;
+    /** The scrollable area into which the collection tree is placed. */
+    private JScrollPane collection_scroll = null;
+    /** The splitpane dividing the collection tree and the metadata editing controls. */
+    private JSplitPane external_split;
+    /** The metadata value table shows the metadata values that are currently assigned to a file. */
+    private MetadataValueTablePane metadata_value_table_pane = null;
+    /** The metadata value tree shows the metadata values that are currently assigned to a metadata element. */
+    private MetadataValueTreePane metadata_value_tree_pane = null;
+
+
+    public EnrichPane()
+    {
+	this.setComponentOrientation(Dictionary.getOrientation());
+        // Create the metadata value tree pane
+	metadata_value_tree_pane = new MetadataValueTreePane();        
+	metadata_value_tree_pane.addMetadataValueTreeSelectionListener(new MetadataValueTreeSelectionListener());
+
+	// Create metadata value table pane
+	metadata_value_table_pane = new MetadataValueTablePane();      
+	metadata_value_table_pane.addMetadataValueTableListSelectionListener(new MetadataValueTableListSelectionListener());
+	metadata_value_table_pane.addMetadataValueTableMouseListener(new MetadataValueTableMouseListener());
+	metadata_value_table_pane.addMetadataValueTextFieldDocumentListener(new MetadataValueTextFieldDocumentListener());
+	metadata_value_table_pane.addMetadataValueTextFieldKeyListener(new MetadataValueTextFieldKeyListener());
+    }
+
+
+    /** Some actions can only occur after this panel has been displayed on-screen, so this method is provided to do exactly that. Such actions include the proportioning of the split panes and the setting of table column widths.
+     */
+    public void afterDisplay()
+    {
+        if (Dictionary.getOrientation().isLeftToRight()){
+            external_split.setDividerLocation(0.3);
+        }else{
+            external_split.setDividerLocation(0.7);
+        }
+    }
+
+
+    /** Used to create, connect and layout the components to be shown on this control panel.
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.file.FileOpenActionListener
+     * @see org.greenstone.gatherer.gui.Filter
+     */
+    public void display()
+    {
+	JPanel left_hand_pane = new JPanel(new BorderLayout());
+        left_hand_pane.setComponentOrientation(Dictionary.getOrientation());
+	GLIButton metadata_set_button = new GLIButton(Dictionary.get("EnrichPane.ManageMetadataSets"), Dictionary.get("EnrichPane.ManageMetadataSets_Tooltip"));
+	metadata_set_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    MetadataSetDialog msd = new MetadataSetDialog();
+		    if (msd.setsChanged()) {
+			valueChanged(null);
+		    } 
+		}
+	    });
+	
+	// Collection Tree
+	collection_pane = new JPanel(new BorderLayout());
+     //   collection_pane.setComponentOrientation(Dictionary.getOrientation());
+	collection_pane.setMinimumSize(MINIMUM_SIZE);
+	collection_pane.setPreferredSize(COLLECTION_TREE_SIZE);
+
+	collection_label = new JLabel(Dictionary.get("Collection.No_Collection"));
+        collection_label.setComponentOrientation(Dictionary.getOrientation());
+	collection_label.setOpaque(true);
+
+	collection_tree = Gatherer.c_man.getCollectionTree();
+	collection_tree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
+	collection_tree.addTreeSelectionListener(this); 
+        collection_tree.setComponentOrientation(Dictionary.getOrientation());
+	collection_filter = collection_tree.getFilter();
+	external_split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
+        external_split.setComponentOrientation(Dictionary.getOrientation());
+	// Layout
+	collection_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(3,3,3,3), BorderFactory.createLoweredBevelBorder()));
+	collection_pane.setMinimumSize(MINIMUM_SIZE);
+	collection_pane.setPreferredSize(new Dimension(Gatherer.g_man.getSize().width / 3, Gatherer.g_man.getSize().height));
+        collection_pane.setComponentOrientation(Dictionary.getOrientation());
+	// Collection Pane
+	collection_pane.add(collection_label, BorderLayout.NORTH);
+
+	JSplitPane metadata_editing_pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+	metadata_editing_pane.setBorder(BorderFactory.createEmptyBorder(0,5,5,5));
+	metadata_editing_pane.setDividerSize(8);
+        metadata_editing_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	metadata_editing_pane.add(metadata_value_table_pane, JSplitPane.TOP);
+	metadata_editing_pane.add(metadata_value_tree_pane, JSplitPane.BOTTOM);
+	metadata_editing_pane.setDividerLocation(250);
+
+	left_hand_pane.add(collection_pane, BorderLayout.CENTER);
+	left_hand_pane.add(metadata_set_button, BorderLayout.SOUTH);
+        if (Dictionary.getOrientation().isLeftToRight()){
+            external_split.add(left_hand_pane, JSplitPane.LEFT);
+            external_split.add(metadata_editing_pane, JSplitPane.RIGHT);
+        }else{
+            external_split.add(left_hand_pane, JSplitPane.RIGHT);
+            external_split.add(metadata_editing_pane, JSplitPane.LEFT);
+        }
+
+
+	
+	this.setLayout(new BorderLayout());
+	this.add(external_split, BorderLayout.CENTER);
+    }
+
+
+    /** Called to inform this pane that it has just gained focus as an effect of the user clicking on its tab
+     */
+    public void gainFocus()
+    {
+	// Remember that we currently have focus
+	has_focus = true;
+
+	// Add the collection tree and filter back into this pane
+	collection_scroll = new JScrollPane(collection_tree);
+	collection_pane.add(collection_scroll, BorderLayout.CENTER);
+	collection_pane.add(collection_filter, BorderLayout.SOUTH);
+
+	// Select the first node in the tree if nothing is already selected
+	if (collection_tree.getSelectionPaths() == null && collection_tree.getRowCount() > 0) {
+ 	    collection_tree.setImmediate(true);
+ 	    collection_tree.setSelectionRow(0);
+ 	    collection_tree.setImmediate(false);
+	    return;
+	}
+
+	// Force all of the controls to be updated
+	valueChanged(null);
+    }
+
+
+    /** Called to inform this pane that it has just lost focus as an effect of the user clicking on another tab
+     */
+    public void loseFocus()
+    {
+	// Very important: make sure metadata value is saved before leaving the pane
+	metadata_value_table_pane.stopEditingAndRebuild(file_nodes);
+
+	// Make sure all the metadata has been saved to file
+	MetadataXMLFileManager.saveMetadataXMLFiles();
+
+	// Remove the collection tree and filter from this pane so it can be added to the Gather pane
+	collection_pane.remove(collection_scroll);
+	collection_pane.remove(collection_filter);
+
+	// Remember that we no longer have focus
+	has_focus = false;
+    }
+
+
+    /** Called whenever the detail mode changes to ensure the filters are at an appropriate level (ie only editable by those that understand regular expression matching)
+     * @param mode the mode level as an int
+     */
+    public void modeChanged(int mode)
+    {
+	collection_filter.setEditable(mode >= Configuration.LIBRARIAN_MODE);
+    }
+
+
+    /** Refresh this pane, depending on what has just happened (refresh_reason). */
+    public void refresh(int refresh_reason, boolean collection_loaded)
+    {
+	if (collection_loaded) {
+	    // Update collection label
+	    collection_label.setText(Dictionary.get("Collection.Collection"));
+	    collection_label.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+	    collection_label.setForeground(Configuration.getColor("coloring.collection_heading_foreground", false));
+
+	    // Update collection tree
+	    if (refresh_reason == Gatherer.COLLECTION_OPENED) {
+		collection_tree.setModel(Gatherer.c_man.getCollectionTreeModel());
+	    }
+
+	    // Update collection filter
+	    collection_filter.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+	}
+	else {
+	    // Update collection label
+	    collection_label.setText(Dictionary.get("Collection.No_Collection"));
+	    collection_label.setBackground(Color.lightGray);
+	    collection_label.setForeground(Color.black);
+
+	    // Update collection tree
+	    collection_tree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode("Error")));
+
+	    // Update collection filter
+	    collection_filter.setBackground(Color.lightGray);
+	}
+
+	// Enable or disable the controls
+	collection_tree.setEnabled(collection_loaded);
+	collection_filter.setEnabled(collection_loaded);
+
+	// Force the metadata table to be rebuilt (for switching extracted metadata on or off)
+	if (refresh_reason == Gatherer.PREFERENCES_CHANGED) {
+	    metadata_value_table_pane.stopEditingAndRebuild(file_nodes);
+	}
+    }
+
+
+  /** Allows other classes to request the enrich pane to prepare for metadata
+   *  saving.
+   *  @author John Thompson, DL Consulting Ltd
+   */
+  public void stopEditingAndRebuild()
+  {
+    // Update the metadata value table (and consequently, the metadata value tree)
+    metadata_value_table_pane.stopEditingAndRebuild(file_nodes);
+  }
+  /** stopEditingAndRebuild() **/
+  
+  /** Called whenever the collection tree selection changes. This causes the metadata value table to be rebuilt. */
+  public void valueChanged(TreeSelectionEvent event)
+    {
+	if(FilenameEncoding.isRefreshRequired()) { 
+		// The CollectionTree is in the process of being re-built to deal with filename-encodings, 
+		// so don't mess up the table during that time.
+		// (CollectionTreeNode.refreshDescendantEncodings() doesn't cope well if this method
+		// responds on Tree selection changes while the tree is updating its encodings, resulting in lost
+		// and misplaced metadata values in the table and thereby in the metadata.xml files themselves).
+		return;
+	}
+	
+	
+	// If we haven't got focus then it must have been a selection in the Gather pane, so don't bother rebuilding
+	if (has_focus == false) {
+	    return;
+	}
+
+	// Nothing selected in the collection tree
+	if (collection_tree.getSelectionCount() == 0) {
+	    file_nodes = null;
+	}
+
+	// Some files selected in the collection tree
+	else {
+	    TreePath paths[] = collection_tree.getSelectionPaths();
+	    file_nodes = new CollectionTreeNode[paths.length];
+	    for (int i = 0; i < paths.length; i++) {
+		file_nodes[i] = (CollectionTreeNode) paths[i].getLastPathComponent();
+	    }
+	}
+
+	// Update the metadata value table (and consequently, the metadata value tree)
+	metadata_value_table_pane.stopEditingAndRebuild(file_nodes);
+    }
+
+
+    private class MetadataValueTableListSelectionListener
+	implements ListSelectionListener
+    {
+	public void valueChanged(ListSelectionEvent list_selection_event)
+	{
+	    // We only want to handle one event per selection, so wait for the value to stabilise
+	    if (list_selection_event.getValueIsAdjusting()) {
+		return;
+	    }
+
+	    // Update the metadata value tree for the current table selection
+	    metadata_value_tree_pane.rebuild(metadata_value_table_pane.getSelectedMetadataValueTableEntry());
+	}
+    }
+
+
+    private class MetadataValueTableMouseListener
+	extends MouseAdapter
+    {
+	public void mouseClicked(MouseEvent mouse_event)
+	{
+	    // We're only interested in clicks on the inherited column
+	    if (metadata_value_table_pane.isMouseEventForInheritedMetadataValueTableColumn(mouse_event) == false) {
+		return;
+	    }
+
+	    // If the selected metadata is inherited, switch to the folder it came from
+	    MetadataValueTableEntry selected_metadata_value_table_entry = metadata_value_table_pane.getSelectedMetadataValueTableEntry();
+	    if (selected_metadata_value_table_entry.isInheritedMetadata()) {
+		collection_tree.setSelection(selected_metadata_value_table_entry.getFolderMetadataInheritedFrom());
+	    }
+	}
+    }
+
+
+    private class MetadataValueTextFieldDocumentListener
+	implements DocumentListener
+    {
+	/** Gives notification that an attribute or set of attributes changed */
+	public void changedUpdate(DocumentEvent document_event) {
+	    validate(document_event);
+	}
+
+	/** Gives notification that there was an insert into the document */
+	public void insertUpdate(DocumentEvent document_event) {
+	    validate(document_event);
+	}
+
+	/** Gives notification that a portion of the document has been removed */
+	public void removeUpdate(DocumentEvent document_event) {
+	    validate(document_event);
+	}
+
+
+	/** Ensures that the value tree is updated in response to changes in the value text field */
+	private void validate(DocumentEvent document_event)
+	{
+	    try {
+		Document document = document_event.getDocument();
+		String metadata_value_string = document.getText(0, document.getLength());
+		metadata_value_tree_pane.selectBestPathForMetadataValue(metadata_value_string);
+	    }
+	    catch (Exception exception) {
+		DebugStream.printStackTrace(exception);
+	    }
+	}
+    }
+
+
+    private class MetadataValueTextFieldKeyListener
+	extends KeyAdapter
+    {
+	/** Gives notification of key events on the text field */
+	public void keyPressed(KeyEvent key_event)
+	{
+	    // Tab: Auto-complete what is selected in the metadata value tree
+	    if (key_event.getKeyCode() == KeyEvent.VK_TAB) {
+		MetadataValueTreeNode selected_metadata_value_tree_node = metadata_value_tree_pane.getSelectedMetadataValueTreeNode();
+		if (selected_metadata_value_tree_node != null) {
+		    metadata_value_table_pane.setMetadataValueTextFieldValue(selected_metadata_value_tree_node.getFullValue());
+		}
+
+		// We do not want this event to be processed by the table also
+		key_event.consume();
+	    }
+
+	    // Enter: save the current value then add a blank row for the selected metadata element
+	    if (key_event.getKeyCode() == KeyEvent.VK_ENTER) {
+		metadata_value_table_pane.stopEditingAndAddBlankRowForSelectedMetadataElement();
+	    }
+	}
+    }
+
+
+    private class MetadataValueTreeSelectionListener
+	implements TreeSelectionListener
+    {
+	public void valueChanged(TreeSelectionEvent tree_selection_event)
+	{
+	    // When a node is selected in the tree, fill the metadata value text field with the selected node's value
+	    MetadataValueTreeNode selected_metadata_value_tree_node = metadata_value_tree_pane.getSelectedMetadataValueTreeNode();
+	    if (selected_metadata_value_tree_node != null) {
+		metadata_value_table_pane.setMetadataValueTextFieldValue(selected_metadata_value_tree_node.getFullValue());
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ExplodeMetadataDatabasePrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ExplodeMetadataDatabasePrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ExplodeMetadataDatabasePrompt.java	(revision 31635)
@@ -0,0 +1,378 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Katherine Don, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+import java.io.*;
+import java.util.ArrayList;
+
+import org.w3c.dom.Document;
+
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.Argument;
+import org.greenstone.gatherer.cdm.ArgumentControl;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.cdm.Plugin;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.collection.ScriptOptions;
+import org.greenstone.gatherer.collection.Collection;
+import org.greenstone.gatherer.feedback.Base64;
+import org.greenstone.gatherer.greenstone.LocalGreenstone;
+import org.greenstone.gatherer.gui.tree.DragTree;
+import org.greenstone.gatherer.metadata.MetadataXMLFileManager;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSet;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.metadata.MetadataTools;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.shell.GShellEvent;
+import org.greenstone.gatherer.shell.GShellListener;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+
+public class ExplodeMetadataDatabasePrompt	
+    extends ModalDialog
+    implements GShellListener
+{
+    /** The size of this new collection dialog box. */
+    static private Dimension SIZE = new Dimension(675, 350);
+    private JDialog self;
+    
+    /** the file we wil be exploding */
+    private File metadata_file = null;
+    /** the list of potential plugins to be used */
+    private Argument plugin_arg = null;
+    /** holds all the available options for the exploding script */
+    private ScriptOptions options = null;
+    /** the pane containing the options */
+    private JPanel options_pane = null;
+    /** the error message if any */
+    private StringBuffer error_message = null;
+    /** whether we were successful or not */
+    private boolean successful;
+
+
+    public ExplodeMetadataDatabasePrompt(File source_file) {
+	super(Gatherer.g_man, true);
+	this.self = this;
+	this.metadata_file = source_file;
+        this.setComponentOrientation(Dictionary.getOrientation());
+	// check that we actually have an explodable file
+	ArrayList exp_plugins = CollectionDesignManager.plugin_manager.getExploderPlugins(source_file);
+	if (exp_plugins.size() == 0) {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("ExplodeMetadataPrompt.NotExplodable"), Dictionary.get("ExplodeMetadataPrompt.Title"), JOptionPane.ERROR_MESSAGE);
+	    return;
+	}
+	plugin_arg = createPluginArgument(exp_plugins);
+	setJMenuBar(new SimpleMenuBar("explodingmetadata"));
+	setSize(SIZE);
+	setTitle(Dictionary.get("ExplodeMetadataPrompt.Title"));
+	
+	// set up the script options
+	// we have empty initial values
+	String dom_string = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Options/>";
+	Document doc = XMLTools.parseXML(new StringReader(dom_string));
+	options = new ScriptOptions(doc.getDocumentElement(), "explode_metadata_database.pl");
+	
+	// Creation
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setOpaque(true);
+	
+	options_pane = new JPanel();
+        options_pane.setComponentOrientation(Dictionary.getOrientation());
+	addScriptOptions(options_pane);
+	JScrollPane middle_pane = new JScrollPane(options_pane);
+        middle_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JTextArea instructions_area = new JTextArea(Dictionary.get("ExplodeMetadataPrompt.Instructions"));
+	instructions_area.setComponentOrientation(Dictionary.getOrientation());
+        instructions_area.setEditable(false);
+	instructions_area.setLineWrap(true);
+	instructions_area.setRows(5);
+	instructions_area.setWrapStyleWord(true);
+	
+	JPanel button_pane = new JPanel();
+	button_pane.setComponentOrientation(Dictionary.getOrientation());
+        JButton explode_button = new GLIButton(Dictionary.get("ExplodeMetadataPrompt.Explode"), Dictionary.get("ExplodeMetadataPrompt.Explode_Tooltip"));
+	JButton cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+	
+	// Connection
+	cancel_button.addActionListener(new CancelListener());
+	explode_button.addActionListener(new ExplodeListener());
+
+	// Layout
+
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	button_pane.setLayout(new GridLayout(1,2));
+	button_pane.add(explode_button);
+	button_pane.add(cancel_button);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(instructions_area, BorderLayout.NORTH);
+	content_pane.add(middle_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	// Final dialog setup & positioning.
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(true);
+    }
+
+    public void destroy() 
+    {
+       	
+    }
+
+    /** All implementation of GShellListener must include this method so the listener can be informed of messages from the GShell.
+     * @param event A <strong>GShellEvent</strong> that contains, amoung other things, the message.
+     */
+    public synchronized void message(GShellEvent event) {
+	String message = event.getMessage();
+	if (message.startsWith("explode_metadata_database.pl>")) {
+	    message = message.substring(29);
+	    error_message.append(message);
+	    error_message.append("\n");
+	}
+    }
+
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell begins its task. Implementation side-effect, not actually used.
+     * @param event A <strong>GShellEvent</strong> that contains details of the initial state of the <strong>GShell</strong> before task comencement.
+     */
+    public synchronized void processBegun(GShellEvent event) {
+	// We don't care. 
+    }
+    
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell completes its task.
+     * @param event A <strong>GShellEvent</strong> that contains details of the final state of the <strong>GShell</strong> after task completion.
+     */
+    public synchronized void processComplete(GShellEvent event) {
+	successful = false;
+	if(event.getStatus() == GShell.OK) {
+	    successful = true;
+	}
+    }
+
+    private Argument createPluginArgument(ArrayList plugin_list) {
+	Argument arg = new Argument();
+	arg.setName("plugin");
+	arg.setDescription(Dictionary.get("ExplodeMetadataPrompt.Plugin"));
+	arg.setRequired(true);
+	arg.setType(Argument.ENUM);
+	for (int i=0; i<plugin_list.size(); i++) {
+	    Plugin p = (Plugin)plugin_list.get(i);
+	    arg.addOption(p.getName(), p.getName());
+	}
+	return arg;
+    }
+    private void addScriptOptions(JPanel options_pane) {
+
+	options_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	options_pane.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+	options_pane.setLayout(new BoxLayout(options_pane, BoxLayout.Y_AXIS));
+
+	int current_mode = Configuration.getMode();
+	int total_argument_count = options.getArgumentCount();
+       
+	// first we add the plugin arg
+	ArgumentControl argument_control = new ArgumentControl(plugin_arg, true, null);
+	argument_control.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+	options_pane.add((JComponent)argument_control);
+	for(int i = 0; i < total_argument_count; i++) {
+	    // Retrieve the argument so we know how to format the control.
+	    Argument argument = options.getArgument(i);
+	    if(!argument.isHiddenGLI() && argument.getModeLevel() <= current_mode) {
+		if (argument.getName().equals("metadata_set")) {
+		    argument.setType(Argument.METADATA_SET_NAMESPACE);
+		    argument_control = new ArgumentControl(argument, true, "Exploded Metadata Set (exp)");
+		}
+		else {
+		    // by default, all args are disabled, and no value
+		    argument_control = new ArgumentControl(argument, false, null);
+		}
+		// make sure they are coloured the way we want - this is not the standard arg control coloring
+		argument_control.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+		options_pane.add((JComponent)argument_control);
+	    }
+	}
+    }
+
+    private void updateScriptOptions() {
+	for(int i = 0; i < options_pane.getComponentCount(); i++) {
+	    Component component = options_pane.getComponent(i);
+	    if(component instanceof ArgumentControl) {
+		ArgumentControl ac = (ArgumentControl)component;
+		String name = ac.getArgumentName();
+		String value = ac.getValue();
+		boolean enabled = ac.isEnabled();
+		if (!enabled && value==null) {
+		    // flag type, remove from options altogether
+		    options.removeValue(name);
+		} else {
+		    if (value.equals("")) {
+			// if the value is empty, we don't want it to be passed to the script
+			options.setValue(name, false, value);
+		    } else {
+			options.setValue(name, enabled, value);
+		    }
+		}
+
+	    }
+	}
+    }
+
+    private int explodeMetadata()
+    {
+	// Generate the explode_metadata_database.pl command
+	ArrayList command_parts_list = new ArrayList();
+	if (!Gatherer.isGsdlRemote) {
+	    command_parts_list.add(Configuration.perl_path);
+	    command_parts_list.add("-S");
+	}
+	command_parts_list.add(LocalGreenstone.getBinScriptDirectoryPath() + "explode_metadata_database.pl");
+	// Add in the options we preset in GLI
+	if (Gatherer.GS3) {
+	    command_parts_list.add("-site");
+	    command_parts_list.add(Configuration.site_name);
+	}
+	command_parts_list.add("-collection");
+	command_parts_list.add(Gatherer.c_man.getCollection ().getName ());
+	command_parts_list.add("-use_collection_plugin_options");
+	// Add in all the options from the user
+	String[] explode_options = options.getValues();
+	for (int i = 0; i < explode_options.length; i++) {
+	    command_parts_list.add(explode_options[i]);
+	}
+
+	// Local case
+	if (!Gatherer.isGsdlRemote) {
+	    // Add in the filename
+	    command_parts_list.add(metadata_file.getPath());
+	}
+	// Remote case
+	else {
+	    // Add in the filename, relative to the collection directory
+	    String collection_name = CollectionManager.getLoadedCollectionName();
+	    String collection_directory_path = CollectionManager.getCollectionDirectoryPath(collection_name);
+	    String metadata_file_relative_path = Gatherer.remoteGreenstoneServer.getPathRelativeToDirectory(
+                                                          metadata_file, collection_directory_path);
+	    metadata_file_relative_path = metadata_file_relative_path.replace(File.separatorChar, '|');
+	    command_parts_list.add("-file");
+	    command_parts_list.add(Base64.encodeBytes(metadata_file_relative_path.getBytes()));
+
+	    // When running remotely we also need the collection name as the last argument
+	    command_parts_list.add(collection_name);
+	}
+
+	// Run the explode_metadata_database.pl command
+	String[] command_parts = (String[]) command_parts_list.toArray(new String[0]);
+	this.error_message = new StringBuffer();
+	GShell process = new GShell(command_parts, GShell.EXPLODE, 3, this, null, GShell.GSHELL_EXPLODE);
+	//process.start();
+	process.run();
+	
+	if (successful) {
+	    return 0;
+	} 
+	return -1;
+    }
+
+    private void resultPrompt(boolean success, String message)
+    {
+	if (success) {
+	    // !!! TO DO: Get explode_metadata_database.pl strings translated and use SimpleResultDialog below
+	    JOptionPane.showMessageDialog(null, Dictionary.get("ExplodeMetadataPrompt.Successful_Explode", metadata_file.getName()), Dictionary.get("ExplodeMetadataPrompt.Successful_Title"), JOptionPane.INFORMATION_MESSAGE);
+	}
+	else {
+	    String title = Dictionary.get("ExplodeMetadataPrompt.Failed_Title");
+	    String label = Dictionary.get("ExplodeMetadataPrompt.Failed_Explode", metadata_file.getName());
+	    SimpleResultDialog result_dialog = new SimpleResultDialog(title, label, message);
+	    result_dialog.setVisible(true); // Blocks
+	    result_dialog.dispose();
+	    result_dialog = null;
+	}
+    }
+
+    private class CancelListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    self.dispose();
+	}
+    }
+    
+    private class ExplodeListener
+	implements ActionListener {
+	
+	public void actionPerformed(ActionEvent event) {
+	    Gatherer.g_man.wait(true);
+	    // update the options
+	    updateScriptOptions();
+	    self.dispose();  
+	    (new ExplodeMetadataDatabaseTask()).start();
+	}
+    }
+
+
+    private class ExplodeMetadataDatabaseTask
+	extends Thread
+    {
+	public ExplodeMetadataDatabaseTask()
+	{
+	}
+
+	public void run()
+	{
+	    int exit_value = explodeMetadata();
+	    if (exit_value == 0) { // success
+		// Clear out the old directory containing the metadata file and download the new directory
+		if (Gatherer.isGsdlRemote) {
+		    Utility.delete(metadata_file.getParentFile());
+		    Gatherer.remoteGreenstoneServer.downloadCollectionFile(
+                             CollectionManager.getLoadedCollectionName(), metadata_file.getParentFile());
+		}
+
+		// Load the new metadata.xml files
+                Collection collection = Gatherer.c_man.getCollection();
+             	MetadataXMLFileManager.loadMetadataXMLFiles(metadata_file.getParentFile(),collection.toSkimFile());
+		Gatherer.g_man.refreshCollectionTree(DragTree.COLLECTION_CONTENTS_CHANGED);
+		resultPrompt(true, error_message.toString());
+	    } else { // failure
+		resultPrompt(false, error_message.toString());
+	    }
+	    error_message = null;
+	    Gatherer.g_man.wait(false);
+	}
+    }    
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ExportAsPrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ExportAsPrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ExportAsPrompt.java	(revision 31635)
@@ -0,0 +1,945 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import javax.swing.*;
+import javax.swing.event.*;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.BasicCollectionConfiguration;
+import org.greenstone.gatherer.greenstone.LocalGreenstone;
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.shell.GShellEvent;
+import org.greenstone.gatherer.shell.GShellListener;
+import org.greenstone.gatherer.shell.GDefaultProgressMonitor;
+import org.greenstone.gatherer.util.ArrayTools;
+import org.greenstone.gatherer.util.CheckList;
+import org.greenstone.gatherer.util.CheckListEntry;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+/** This class provides the functionality to export a set of current
+ * collections from the GSDLHOME/collect/ directory to various formats
+ * (hence ExportAs) using export.pl. The user chooses the collection from a
+ * list, where each entry also displays details about itself, confirms the
+ * delete of a collection by checking a checkbox then presses the ok button
+ * to actually delete the collection.  
+ * Copied from WriteCDImagePrompt
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class ExportAsPrompt
+    extends ModalDialog 
+    implements GShellListener {
+
+    static final private Dimension LABEL_SIZE = new Dimension(120, 25);
+    
+    private OKButtonListener ok_button_listener;
+
+    private JLabel saveas_label = null;
+    private JComboBox saveas_combobox = null;
+
+    private ArrayList all_collections = null;
+    private BasicCollectionConfiguration selected_collection = null;
+  /** The list of collections to export */
+    private JList list = null;
+    /** The currently selected collection for deletion. */
+    private BasicCollectionConfiguration collection = null;
+    /** A reference to ourself so any inner-classes can dispose of us. */
+    private ExportAsPrompt prompt = null;
+    /** The close button, which exits the prompt without deleting anything. */
+    private JButton cancel_button = null;
+    /** The ok button which causes the selected collection to be deleted. */
+    private JButton ok_button = null;
+    /** The label above details. */
+    private JLabel details_label = null;
+    /** The label above the list. */
+    private JLabel list_label = null;
+    /** The text area used to display details about the collection selected. */
+    private JTextArea details_textarea = null;
+    /** The text area used to display instructions for the cd-rom/dvd export */
+    private JTextArea instructions_textarea;
+    /** A string array used to pass arguments to the phrase retrieval method. */
+    private JTextField title_field = null;
+    private JLabel title_label = null;
+    private String args[] = null;
+    private String cd_title = null;
+    /** whether the exporting was successful or not */
+    private boolean successful = false;
+    /** whether we are trying to export or not */
+    private boolean exporting = false;
+    /** the error message if any */
+    private StringBuffer error_message = null;
+    /** The size of the export prompt screen. */
+    public static final Dimension SIZE = new Dimension(500, 540);  
+    private GDefaultProgressMonitor progress_monitor;    
+
+    private JButton convert_xml_button1 = null;
+    private JButton convert_xml_button2 = null;
+    private JButton convert_xml_button3 = null;
+    private JButton folder_button = null;
+
+    private JPanel instructions_pane = null;
+    
+    private JPanel convert_xml_pane1 = null;
+    private JPanel convert_xml_pane2 = null;
+    private JPanel mapping_xml_pane = null;
+
+    private JCheckBox convert_xml_checkbox1 = null; 
+    private JCheckBox convert_xml_checkbox2 = null;
+    private JCheckBox mapping_xml_checkbox = null;
+    
+    private JCheckBox output_single_checkbox = null;  
+
+    private JTextField convert_xml_field1 = null;
+    private JTextField convert_xml_field2 = null;
+    private JTextField mapping_xml_field = null;
+
+
+    private File xsl_file1 = null;
+    private File xsl_file2 = null;
+    private File mapping_file = null;
+
+    private JPanel convert_xml_pane = null; 
+
+    private HashMap plugoutMap = new HashMap();
+  
+
+    /** Constructor.
+     * @see org.greenstone.gatherer.collection.ExportAsPrompt.CancelButtonListener
+     * @see org.greenstone.gatherer.collection.ExportAsPrompt.CollectionListListener
+     * @see org.greenstone.gatherer.collection.ExportAsPrompt.OKButtonListener
+     */
+    public ExportAsPrompt() {
+	super(Gatherer.g_man, true);
+	this.setComponentOrientation(Dictionary.getOrientation());
+	plugoutMap.clear();         
+	plugoutMap.put("DSpace","dublin-core.xml");
+        plugoutMap.put("MARCXML","doc.xml");   
+	plugoutMap.put("GreenstoneMETS","doctxt.xml,docmets.xml");     
+	plugoutMap.put("FedoraMETS","doctxt.xml,docmets.xml");     
+
+	// this is the order we want them to appear in the list
+	String [] saveas_formats = {"GreenstoneMETS", "FedoraMETS", "MARCXML", "DSpace"};
+        cancel_button = new GLIButton(Dictionary.get("General.Close"), Dictionary.get("General.Close_Tooltip"));
+	
+	details_textarea = new JTextArea(Dictionary.get("DeleteCollectionPrompt.No_Collection"));
+	details_textarea.setEditable(false);
+	details_textarea.setComponentOrientation(Dictionary.getOrientation());
+        
+	details_label = new JLabel(Dictionary.get("DeleteCollectionPrompt.Collection_Details"));
+	details_label.setComponentOrientation(Dictionary.getOrientation());
+
+	instructions_textarea = new JTextArea(Dictionary.get("ExportAsPrompt.Instructions"));
+	instructions_textarea.setCaretPosition(0);
+	instructions_textarea.setEditable(false);
+	instructions_textarea.setLineWrap(true);
+	instructions_textarea.setRows(4);
+	instructions_textarea.setWrapStyleWord(true);
+	instructions_textarea.setComponentOrientation(Dictionary.getOrientation());
+             
+       
+        saveas_label = new JLabel(Dictionary.get("ExportAsPrompt.SaveAs"));
+        saveas_label.setComponentOrientation(Dictionary.getOrientation());
+        //saveas_label.setPreferredSize(LABEL_SIZE);
+        
+	saveas_combobox = new JComboBox(saveas_formats);
+	saveas_combobox.setOpaque(false);
+	saveas_combobox.setToolTipText(Dictionary.get("ExportAsPrompt.SaveAs_Tooltip"));
+        saveas_combobox.setComponentOrientation(Dictionary.getOrientation());
+
+	// Add xml conversion feature
+	convert_xml_button1 = new GLIButton(Dictionary.get("ExportAsPrompt.Browse"),Dictionary.get("ExportAsPrompt.Browse_Tooltip"));
+	convert_xml_button1.setEnabled(false);
+
+	convert_xml_button2 = new GLIButton(Dictionary.get("ExportAsPrompt.Browse"),Dictionary.get("ExportAsPrompt.Browse_Tooltip"));
+	convert_xml_button2.setEnabled(false);
+	
+	convert_xml_button3 = new GLIButton(Dictionary.get("ExportAsPrompt.Browse"),Dictionary.get("ExportAsPrompt.Browse_Tooltip"));
+	convert_xml_button3.setEnabled(false);
+
+	convert_xml_checkbox1 = new JCheckBox();
+        convert_xml_checkbox1.setText(Dictionary.get("ExportAsPrompt.ApplyXSL","doc.xml"));
+        convert_xml_checkbox1.setToolTipText(Dictionary.get("ExportAsPrompt.ApplyXSL_Tooltip"));
+        convert_xml_checkbox1.setComponentOrientation(Dictionary.getOrientation());
+        
+	convert_xml_checkbox2 = new JCheckBox();
+        convert_xml_checkbox2.setToolTipText(Dictionary.get("ExportAsPrompt.ApplyXSL_Tooltip"));
+        convert_xml_checkbox2.setComponentOrientation(Dictionary.getOrientation());
+        
+        output_single_checkbox = new JCheckBox();
+        output_single_checkbox.setComponentOrientation(Dictionary.getOrientation());
+	output_single_checkbox.setText(Dictionary.get("ExportAsPrompt.MARCXMLGroup"));
+        output_single_checkbox.setToolTipText(Dictionary.get("ExportAsPrompt.MARCXMLGroup_Tooltip"));
+
+
+        mapping_xml_checkbox = new JCheckBox();
+        mapping_xml_checkbox.setComponentOrientation(Dictionary.getOrientation());
+	mapping_xml_checkbox.setText(Dictionary.get("ExportAsPrompt.MappingXML"));
+        mapping_xml_checkbox.setToolTipText(Dictionary.get("ExportAsPrompt.MappingXML_Tooltip"));
+
+        convert_xml_field1 = new JTextField();
+        convert_xml_field1.setComponentOrientation(Dictionary.getOrientation());
+	convert_xml_field1.setEnabled(false);
+
+	convert_xml_field2 = new JTextField();
+        convert_xml_field2.setComponentOrientation(Dictionary.getOrientation());
+	convert_xml_field2.setEnabled(false);
+
+	mapping_xml_field = new JTextField();
+        mapping_xml_field.setComponentOrientation(Dictionary.getOrientation());
+	mapping_xml_field.setEnabled(false);
+
+        convert_xml_pane1 = new JPanel(new BorderLayout());
+	convert_xml_pane1.setComponentOrientation(Dictionary.getOrientation());                
+        
+	convert_xml_pane2 = new JPanel(new BorderLayout());
+        convert_xml_pane2.setComponentOrientation(Dictionary.getOrientation());
+        
+	mapping_xml_pane = new JPanel(new BorderLayout());
+        mapping_xml_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	convert_xml_pane = new JPanel(new GridLayout(3,1));
+        convert_xml_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	all_collections = new ArrayList();
+	//list = new CheckList(true);
+	list = new JList(getCollectionListModel());
+	list.setComponentOrientation(Dictionary.getOrientation());
+	list_label = new JLabel(Dictionary.get("DeleteCollectionPrompt.Collection_List"));
+	list_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	ok_button = new GLIButton(Dictionary.get("ExportAsPrompt.Export"), Dictionary.get("ExportAsPrompt.Export_Tooltip"));
+	
+	title_field = new JTextField();
+        title_field.setComponentOrientation(Dictionary.getOrientation());
+	title_field.setToolTipText(Dictionary.get("ExportAsPrompt.Export_Name_Tooltip"));
+	title_label = new JLabel(Dictionary.get("ExportAsPrompt.Export_Name"));
+        title_label.setComponentOrientation(Dictionary.getOrientation());
+        
+        folder_button = new GLIButton(Dictionary.get("ExportAsPrompt.Browse"),Dictionary.get("ExportAsPrompt.Browse_Tooltip"));
+	folder_button.addActionListener(new FolderButtonListener());
+   
+       
+	prompt = this;
+	setSize(SIZE);
+	setTitle(Dictionary.get("ExportAsPrompt.Title"));
+
+	setJMenuBar(new SimpleMenuBar("exporting")); 
+	cancel_button.addActionListener(new CancelButtonListener());
+	list.addListSelectionListener(new CollectionListListener());
+	list.clearSelection();
+	list.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+	ok_button_listener = new OKButtonListener();
+	ok_button.addActionListener(ok_button_listener);
+	ok_button.setEnabled(false);
+	//title.getDocument().addDocumentListener(new DocumentListener());
+
+       	convert_xml_button1.addActionListener(new ConvertXMLButtonListener());
+	convert_xml_checkbox1.addActionListener(new ConvertXMLCheckboxListener());
+	mapping_xml_checkbox.addActionListener(new ConvertXMLCheckboxListener());
+	convert_xml_button3.addActionListener(new ConvertXMLButtonListener());
+   
+	convert_xml_button2.addActionListener(new ConvertXMLButtonListener());
+	convert_xml_checkbox2.addActionListener(new ConvertXMLCheckboxListener());
+
+        saveas_combobox.addActionListener(new SaveasListener());        
+
+    }
+
+    /** Destructor. */
+    public void destroy() {
+        saveas_label = null;
+	saveas_combobox = null;
+	all_collections.clear();
+	all_collections = null;
+	cancel_button = null;
+	details_textarea = null;
+	details_label = null;
+	list = null;
+	ok_button = null;
+	prompt = null;
+	selected_collection = null;
+	title_field = null;
+	title_label = null;
+    }
+
+    /** This method causes the modal prompt to be displayed. 
+     * returns true if it has exported the collections that are currently selected */
+    public boolean display() {
+        JScrollPane scrol_tmp;
+	// Top pane
+	instructions_pane = new JPanel(new BorderLayout());
+        instructions_pane.setComponentOrientation(Dictionary.getOrientation());
+        instructions_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,5));
+        scrol_tmp = new JScrollPane(instructions_textarea);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+        instructions_pane.add(scrol_tmp, BorderLayout.CENTER);
+	
+	title_label.setBorder(BorderFactory.createEmptyBorder(0,5,0,15));
+
+	JPanel title_pane = new JPanel(new BorderLayout());
+        title_pane.setComponentOrientation(Dictionary.getOrientation());
+	title_pane.add(title_label, BorderLayout.LINE_START);
+	title_pane.add(title_field, BorderLayout.CENTER);
+        title_pane.add(folder_button, BorderLayout.LINE_END);
+        //apply xsl pane
+          
+	convert_xml_pane1.removeAll();
+        convert_xml_pane2.removeAll();
+	mapping_xml_pane.removeAll();
+	convert_xml_pane.removeAll();
+
+	convert_xml_pane1.add(convert_xml_checkbox1, BorderLayout.LINE_START);
+	convert_xml_pane1.add(convert_xml_field1, BorderLayout.CENTER);
+	convert_xml_pane1.add(convert_xml_button1, BorderLayout.LINE_END);
+
+	convert_xml_pane2.add(convert_xml_checkbox2, BorderLayout.LINE_START);
+	convert_xml_pane2.add(convert_xml_field2, BorderLayout.CENTER);
+	convert_xml_pane2.add(convert_xml_button2, BorderLayout.LINE_END);
+      
+	mapping_xml_pane.add(mapping_xml_checkbox, BorderLayout.LINE_START);
+	mapping_xml_pane.add(mapping_xml_field, BorderLayout.CENTER);
+	mapping_xml_pane.add(convert_xml_button3, BorderLayout.LINE_END);
+
+	convert_xml_pane.add(convert_xml_pane1);
+       	
+        String saveas = (String)saveas_combobox.getSelectedItem();
+	// force the updating of which fields we are displaying
+	saveas_combobox.setSelectedIndex(0);
+
+        // Save as pane
+	JPanel saveas_pane = new JPanel(new BorderLayout());
+        saveas_pane.setComponentOrientation(Dictionary.getOrientation());
+	saveas_label.setBorder(BorderFactory.createEmptyBorder(0,5,0,15));
+	saveas_pane.add(saveas_label, BorderLayout.LINE_START);
+	saveas_pane.add(saveas_combobox, BorderLayout.CENTER);
+
+        JPanel tmp_pane = new JPanel(new BorderLayout());
+        tmp_pane.setComponentOrientation(Dictionary.getOrientation());
+        tmp_pane.add(saveas_pane, BorderLayout.NORTH);
+	tmp_pane.add(title_pane, BorderLayout.CENTER);
+
+	instructions_pane.add(tmp_pane, BorderLayout.NORTH);          
+
+	instructions_pane.add(convert_xml_pane, BorderLayout.CENTER);
+
+	// Central pane
+	JPanel list_pane = new JPanel(new BorderLayout());
+        list_pane.setComponentOrientation(Dictionary.getOrientation());
+	list_pane.add(list_label, BorderLayout.NORTH);
+        scrol_tmp = new JScrollPane(list);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	list_pane.add(scrol_tmp, BorderLayout.CENTER);
+	list_pane.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
+
+	JPanel details_pane = new JPanel(new BorderLayout());
+        details_pane.setComponentOrientation(Dictionary.getOrientation());
+	details_pane.add(details_label, BorderLayout.NORTH);
+        scrol_tmp = new JScrollPane(details_textarea);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	details_pane.add(scrol_tmp, BorderLayout.CENTER);
+	details_pane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
+
+	JPanel central_pane = new JPanel(new GridLayout(2, 1));
+        central_pane.setComponentOrientation(Dictionary.getOrientation());
+	central_pane.add(list_pane);
+	central_pane.add(details_pane);
+	central_pane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+
+	// Lower pane
+	JPanel button_pane = new JPanel(new GridLayout(1, 2));
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	button_pane.add(ok_button);
+	button_pane.add(cancel_button);
+	button_pane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
+
+	JPanel lower_pane = new JPanel(new BorderLayout());
+        lower_pane.setComponentOrientation(Dictionary.getOrientation());
+	lower_pane.add(button_pane, BorderLayout.SOUTH);
+	lower_pane.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5));
+
+	// Final.
+	JPanel content_pane = (JPanel)this.getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(instructions_pane, BorderLayout.NORTH);
+	content_pane.add(central_pane, BorderLayout.CENTER);
+	content_pane.add(lower_pane, BorderLayout.SOUTH);
+
+	// Center and display.
+	Dimension screen_size = Configuration.screen_size;
+	this.setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	this.setVisible(true); // blocks until the dialog is killed
+	return true;
+	
+    }
+
+    
+    /** This method calls the builcol.pl scripts via a GShell so as to not lock up the processor.
+     * @see org.greenstone.gatherer.Configuration
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.Collection
+     * @see org.greenstone.gatherer.gui.BuildOptions
+     * @see org.greenstone.gatherer.shell.GShell
+     * @see org.greenstone.gatherer.shell.GShellListener
+     * @see org.greenstone.gatherer.shell.GShellProgressMonitor
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    public void exportAsCollections() {
+	DebugStream.println("ExportAsPrompt.exportAsCollections()");
+
+	if (selected_collection == null) return;
+
+	cd_title = title_field.getText();
+
+	String export_type = (String) saveas_combobox.getSelectedItem();
+      
+	// Generate the export.pl command
+	ArrayList command_parts_list = new ArrayList();
+	if (!Gatherer.isGsdlRemote) {
+	    command_parts_list.add(Configuration.perl_path);
+	    command_parts_list.add("-S");
+	}
+	command_parts_list.add(LocalGreenstone.getBinScriptDirectoryPath() + "export.pl");
+	command_parts_list.add("-gli");
+	command_parts_list.add("-language");
+	command_parts_list.add(Configuration.getLanguage());
+	command_parts_list.add("-removeold");
+	command_parts_list.add("-saveas");
+	command_parts_list.add(export_type);
+	// we want to be able to export items from other collect directories too
+	String collectDir = Gatherer.getCollectDirectoryPath();
+
+	if(Gatherer.GS3) {
+	    command_parts_list.add("-site");
+	    command_parts_list.add(Configuration.site_name);
+	}
+
+	if(collectDir != Gatherer.getDefaultGSCollectDirectoryPath(true)) {
+		command_parts_list.add("-collectdir");
+		command_parts_list.add(collectDir);
+	}	
+	command_parts_list.add("-exportdir");
+	String export_dir = LocalGreenstone.getTmpDirectoryPath();
+       	if (cd_title.equals("")) {
+            export_dir += "exported_" + selected_collection.getShortName()+ "_" +export_type;
+	}
+	else {
+	    File cd_file = new File(cd_title);
+            if (cd_file.isAbsolute())
+		export_dir = cd_title + File.separator + "exported_"  + selected_collection.getShortName()+ "_"+ export_type;
+	    else{
+                cd_title = cd_title.replaceAll("\\s+","");
+                cd_title = cd_title.replaceAll("\\\\+","/");
+                cd_title = cd_title.replaceAll("/+","/");
+            	export_dir +=cd_title;
+	    }
+	}
+
+        command_parts_list.add(export_dir);
+
+	// check the xslt/mapping file values.
+	
+	// now we build up the saveas options
+	StringBuffer saveas_options = new StringBuffer();
+	
+	if (export_type.equals("MARCXML")){
+	  if (!convert_xml_field1.getText().equals("")) {
+	    saveas_options.append("-xslt_file ");
+	    saveas_options.append(convert_xml_field1.getText()+" "); 
+	  }
+	  if (!mapping_xml_field.getText().equals("")) {
+	      saveas_options.append("-mapping_file ");
+	      saveas_options.append(mapping_xml_field.getText()+" ");
+	  }
+	    if (output_single_checkbox.isSelected()){
+	      saveas_options.append("-group ");
+	    }
+	}
+	else if (export_type.endsWith("METS")) {
+	  if (!convert_xml_field1.getText().equals("")) {
+	    saveas_options.append("-xslt_txt ");
+	    saveas_options.append(convert_xml_field1.getText()+" "); 
+	  }
+	  if (!convert_xml_field2.getText().equals("")) {
+	    saveas_options.append("-xslt_mets ");
+	    saveas_options.append(convert_xml_field2.getText()+" "); 
+	  }
+	}
+	else if (export_type.equals("DSpace")) {
+	  if (!convert_xml_field1.getText().equals("")) {
+	    saveas_options.append("-xslt_file ");
+	    saveas_options.append(convert_xml_field1.getText()+" "); 
+	  }
+	}
+       
+	 if (saveas_options.length()>0) {
+	     command_parts_list.add("-saveas_options");
+	     command_parts_list.add(saveas_options.toString());
+	 }
+	 
+	 command_parts_list.add( selected_collection.getShortName());
+	 
+
+	DebugStream.print("export command = ");
+	for (int i = 0; i < command_parts_list.size(); i++) {
+	    DebugStream.print(command_parts_list.get(i) + " ");
+	    //System.err.print("'"+command_parts_list.get(i)+"'" + " ");          
+	}
+	DebugStream.println("");
+
+	// Run the export.pl command
+	String[] command_parts = (String[]) command_parts_list.toArray(new String[0]);
+
+        progress_monitor = new  GDefaultProgressMonitor();
+
+	GShell process = new GShell(command_parts, GShell.EXPORTAS, 3, this,progress_monitor , GShell.GSHELL_EXPORTAS);
+	process.start();
+	//process.run();
+	DebugStream.println("ExportAsPrompt.exportAsCollections().return");
+	
+    }
+
+
+    public void cancelExporting(){
+	progress_monitor.setStop(true);           
+    }
+
+
+    /** Shows an export complete prompt. 
+     * @param success A <strong>boolean</strong> indicating if the collection was successfully deleted.
+     * @see org.greenstone.gatherer.collection.Collection
+     */
+    public void resultPrompt(boolean success, String extra) {
+	args = new String[2];
+	
+	// coll name
+	args[0] = selected_collection.getName() + StaticStrings.SPACE_CHARACTER + StaticStrings.OPEN_PARENTHESIS_CHARACTER + selected_collection.getShortName() + StaticStrings.CLOSE_PARENTHESIS_CHARACTER;
+	
+	String export_type = (String) saveas_combobox.getSelectedItem();       
+       	args[1] = LocalGreenstone.getTmpDirectoryPath();
+       	if (cd_title.equals("")) {
+           args[1] += "exported_" + selected_collection.getShortName()+"_"+export_type;
+	}
+	else {
+	    File cd_file = new File(cd_title);
+            if (cd_file.isAbsolute())
+		args[1] = cd_title + File.separator + "exported_" + selected_collection.getShortName()+"_"+export_type;
+	    else{
+                cd_title = cd_title.replaceAll("\\s+","");
+                cd_title = cd_title.replaceAll("\\\\+","/");
+                cd_title = cd_title.replaceAll("/+","/");
+            	args[1] +=cd_title;
+	    }
+	}
+        
+
+	String title;
+	String label;
+	String details;
+
+	if (success) {
+	  String successMessage = "ExportAsPrompt.Successful_ExportOne";
+	  title = Dictionary.get("ExportAsPrompt.Successful_Title");
+	  label = Dictionary.get(successMessage, args);
+	  details = Dictionary.get("ExportAsPrompt.Successful_Details", args);
+	} else {
+	  String failedMessage = "ExportAsPrompt.Failed_ExportOne";
+	  title = Dictionary.get("ExportAsPrompt.Failed_Title");
+	  label = Dictionary.get(failedMessage, args);
+	  details = Dictionary.get("ExportAsPrompt.Failed_Details", args);
+	}
+	SimpleResultDialog result_dialog = new SimpleResultDialog(this, title, label, details);
+	result_dialog.setVisible(true); // Blocks
+	result_dialog.dispose();
+	result_dialog = null;
+    }
+
+    /** Method to scan the collect directory retrieving and reloading each collection it finds, while building the list of known collections.
+     * @see org.greenstone.gatherer.Configuration
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.util.ArrayTools
+     * @see org.greenstone.gatherer.util.Utility
+     */
+  private ListModel getCollectionListModel() {
+    DefaultListModel model = new DefaultListModel();
+	// Start at the collect dir.
+	File collect_directory = new File(Gatherer.getCollectDirectoryPath());
+	String file_name = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+	if (collect_directory.exists()) {
+	    // Now for each child directory see if it contains a .col file and
+	    // if so try to load it..
+	    File collections[] = collect_directory.listFiles();
+	    ArrayTools.sort(collections);
+	    for(int i = 0; collections != null && i < collections.length; i++) {
+		if(collections[i].isDirectory() && !collections[i].getName().equals(StaticStrings.MODEL_COLLECTION_NAME)) {
+		    File config_file = new File(collections[i], file_name);
+		    if (config_file.exists()) {
+			BasicCollectionConfiguration config = new BasicCollectionConfiguration(config_file);
+			model.addElement(config);		        
+			config = null;
+		    }
+		}
+	    }
+	}
+	return model;
+	// Otherwise the collect directory doesn't actually exist, so there ain't much we can do.
+  }
+
+
+    /** All implementation of GShellListener must include this method so the listener can be informed of messages from the GShell.
+     * @param event A <strong>GShellEvent</strong> that contains, amoung other things, the message.
+     */
+    public synchronized void message(GShellEvent event) {
+	// Ignore the messages from RecPlug with 'show_progress' set (used for progress bars)
+	String message = event.getMessage();
+	if (message.startsWith("export.pl>")) {
+	    message = message.substring(13);
+	    //DebugStream.println("message = "+event.getMessage());
+	    error_message.append(message);
+	    error_message.append("\n");
+	}
+    }
+
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell begins its task. Implementation side-effect, not actually used.
+     * @param event A <strong>GShellEvent</strong> that contains details of the initial state of the <strong>GShell</strong> before task comencement.
+     */
+    public synchronized void processBegun(GShellEvent event) {
+	// We don't care. 
+    }
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell completes its task.
+     * @param event A <strong>GShellEvent</strong> that contains details of the final state of the <strong>GShell</strong> after task completion.
+     */
+    public synchronized void processComplete(GShellEvent event) {
+	successful = false;
+	if(event.getStatus() == GShell.OK) {
+	    if(event.getType() == GShell.EXPORTAS) {
+		successful = true;
+	    }
+	}
+	ok_button_listener.processComplete();
+    }
+
+    /** A button listener implementation, which listens for actions on the close button and disposes of the dialog when detected. */
+    private class CancelButtonListener 
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so we can be informed when the button is actioned.
+	 * @param event An <strong>ActionEvent</strong> containing all the relevant information garnered from the event itself.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    prompt.dispose();
+	}
+    }
+
+    /** This private class listens for selection events in from the list and then displays the appropriate details for that collection.
+     */
+    private class CollectionListListener 
+	implements ListSelectionListener
+    {
+	/** Any implementation of ListSelectionListener must include this method so we can be informed when the list selection changes.
+	 * @param  event a <strong>ListSelectionEvent</strong> containing all the relevant information garnered from the event itself
+	 */
+	public void valueChanged(ListSelectionEvent event)
+	{
+	    // Can only export when something is ticked
+	    //ok_button.setEnabled(!list.isNothingTicked());
+
+	    if (list.isSelectionEmpty()) {
+		details_textarea.setText(Dictionary.get("DeleteCollectionPrompt.No_Collection"));
+		ok_button.setEnabled(false);
+	    }
+	    else {
+	      BasicCollectionConfiguration collection = (BasicCollectionConfiguration) list.getSelectedValue();
+	      args = new String[3];
+	      args[0] = collection.getCreator();
+	      args[1] = collection.getMaintainer();
+	      args[2] = collection.getDescription();
+	      details_textarea.setText(Dictionary.get("DeleteCollectionPrompt.Details", args));
+	      details_textarea.setCaretPosition(0);
+	      ok_button.setEnabled(true);
+	    }
+	}
+
+    }
+    /** The OK button listener implementation. */
+    private class OKButtonListener 
+	implements ActionListener {
+	private Component glass_pane;
+	private MouseListener mouse_blocker_listener;
+	private ProgressDialog progress_dialog;
+
+	/** Any implementation of ActionListener must include this method so we can be informed when the button is actioned.
+	 * @param event An <strong>ActionEvent</strong> containing all the relevant information garnered from the event itself.
+	 * @see org.greenstone.gatherer.Configuration
+	 * @see org.greenstone.gatherer.Gatherer
+	 * @see org.greenstone.gatherer.util.Utility
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    ///ystem.err.println("OK Clicked");
+	    // Make sure there are some colls specified
+	    selected_collection = (BasicCollectionConfiguration)list.getSelectedValue();
+	    error_message = new StringBuffer();
+	    
+	    // Set the cursor to hourglass
+	    glass_pane = getGlassPane();
+	    mouse_blocker_listener = new MouseAdapter() {};
+	    glass_pane.addMouseListener(mouse_blocker_listener);
+	    glass_pane.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+	    glass_pane.setVisible(true);
+
+	    // Export the selected collection.
+	    ///ystem.err.println("Exporting As for named collections");
+	    exportAsCollections();
+
+	    // Show progress dialog
+	    ///ystem.err.println("Showing progress dialog");
+	    progress_dialog = new ProgressDialog();
+	    progress_dialog.setVisible(true);
+	}
+
+	public void processComplete() {
+	    ///ystem.err.println("Process complete");
+	    // Dispose of progress dialog
+   
+	  
+	    progress_dialog.setVisible(false);
+	    progress_dialog.dispose();
+	    progress_dialog = null;
+
+	    // unset the cursor
+	    glass_pane.setVisible(false);
+	    glass_pane.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+	    glass_pane.removeMouseListener(mouse_blocker_listener);
+	    glass_pane = null;
+	    mouse_blocker_listener= null;
+	    
+	    if (successful) {
+		resultPrompt(true, error_message.toString());
+	    } else {
+		resultPrompt(false, error_message.toString());
+	    }
+	    error_message = null;
+             
+            convert_xml_button1.setEnabled(false); 
+            xsl_file1 = null;
+            convert_xml_field1.setText("");
+            convert_xml_checkbox1.setSelected(false);
+
+	    convert_xml_button2.setEnabled(false); 
+            xsl_file2 = null;
+            convert_xml_field2.setText("");
+            convert_xml_checkbox2.setSelected(false);
+
+	    mapping_xml_checkbox.setSelected(false);
+	    output_single_checkbox.setSelected(false);
+	    convert_xml_button3.setEnabled(false);
+            mapping_xml_field.setText("");
+	    mapping_file = null;
+
+	}
+
+	private class ProgressDialog 
+	    extends ModalDialog {
+	    
+	    private Dimension size = new Dimension(400,110);
+	    
+	    public ProgressDialog() {
+		super(Gatherer.g_man, Dictionary.get("ExportAsPrompt.Title"), true);
+		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+		setSize(size);
+		JPanel content_pane = (JPanel) getContentPane();
+		JLabel progress_label = new JLabel(Dictionary.get("ExportAsPrompt.Progress_Label"));
+		
+		JProgressBar progress_bar = new JProgressBar();
+		progress_bar.setIndeterminate(true);
+		content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+		GLIButton cancel_button = new GLIButton(Dictionary.get("ExportAsPrompt.Cancel"),Dictionary.get("ExportAsPrompt.Cancel_Tooltip"));
+		cancel_button.setMnemonic(KeyEvent.VK_C);
+	
+		cancel_button.addActionListener(new ActionListener(){
+			public void actionPerformed(ActionEvent e){
+			    cancelExporting();
+			}
+
+                });
+
+                JPanel cancel_panel = new JPanel(new FlowLayout(FlowLayout.CENTER,0,0));
+		cancel_panel.add(cancel_button);
+                                    
+		content_pane.setLayout(new BorderLayout(0,5));
+		content_pane.add(progress_label, BorderLayout.NORTH);
+		content_pane.add(progress_bar, BorderLayout.CENTER);
+		content_pane.add(cancel_panel, BorderLayout.SOUTH);
+
+		// Position
+		Rectangle frame_bounds = Gatherer.g_man.getBounds();
+		setLocation(frame_bounds.x + (frame_bounds.width - size.width) / 2, frame_bounds.y + (frame_bounds.height - size.height) / 2);
+	    }
+	}
+    }
+
+   private class ConvertXMLCheckboxListener implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	  
+	    convert_xml_button1.setEnabled(convert_xml_checkbox1.isSelected());
+	    convert_xml_field1.setEnabled(convert_xml_checkbox1.isSelected());
+	    convert_xml_button2.setEnabled(convert_xml_checkbox2.isSelected());
+	    convert_xml_field2.setEnabled(convert_xml_checkbox2.isSelected());
+	    convert_xml_button3.setEnabled(mapping_xml_checkbox.isSelected());
+	    mapping_xml_field.setEnabled(mapping_xml_checkbox.isSelected());
+			
+	}
+    }
+
+
+     
+    private class SaveasListener implements ActionListener {
+    
+	public void actionPerformed(ActionEvent event) {
+		
+	    convert_xml_checkbox1.setSelected(false);
+	    convert_xml_checkbox2.setSelected(false);
+	    mapping_xml_checkbox.setSelected(false);
+	    output_single_checkbox.setSelected(false);
+
+	    convert_xml_button1.setEnabled(false);
+            convert_xml_button2.setEnabled(false);
+	    convert_xml_button3.setEnabled(false);
+
+            convert_xml_field1.setText("");
+	    convert_xml_field2.setText("");
+	    mapping_xml_field.setText("");
+	    
+	    String saveas = (String)saveas_combobox.getSelectedItem();
+
+	    if (convert_xml_pane.getComponentCount() > 1){
+		convert_xml_pane.remove(1);
+		if (convert_xml_pane.getComponentCount() > 1){
+		    convert_xml_pane.remove(1);
+		}
+	    }
+
+	    if (!saveas.endsWith("METS")){
+	
+		convert_xml_checkbox1.setText(Dictionary.get("ExportAsPrompt.ApplyXSL",(String)plugoutMap.get(saveas)));	
+		if (saveas.equals("MARCXML")){
+		    convert_xml_pane.add(mapping_xml_pane);
+		    convert_xml_pane.add(output_single_checkbox);
+		}		
+	    }
+	    else{
+		String[] docs = ((String)plugoutMap.get(saveas)).split(",");
+		convert_xml_checkbox1.setText(Dictionary.get("ExportAsPrompt.ApplyXSL",docs[0]));
+		convert_xml_checkbox2.setText(Dictionary.get("ExportAsPrompt.ApplyXSL",docs[1])); 
+		convert_xml_pane.add(convert_xml_pane2); 		
+	    }
+	    
+	    convert_xml_pane.revalidate();
+	    convert_xml_pane.repaint();
+            instructions_pane.revalidate();
+            instructions_pane.repaint();
+             
+	}
+	
+    }
+       
+     private class ConvertXMLButtonListener implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    JFileChooser chooser = new JFileChooser();
+	    // Note: source for ExampleFileFilter can be found in FileChooserDemo,
+	    // under the demo/jfc directory in the Java 2 SDK, Standard Edition.
+	    javax.swing.filechooser.FileFilter filter = new javax.swing.filechooser.FileFilter(){
+		    public boolean accept(File f){
+			return f.getPath().endsWith(".xsl")||f.isDirectory()||f.getPath().endsWith(".xml");
+		    }  
+
+		    public String getDescription(){
+			return "XSL or XML file";
+		    }          
+      };
+	    
+	    chooser.setFileFilter(filter);
+	    int returnVal = chooser.showOpenDialog(prompt);        
+
+	    if(returnVal == JFileChooser.APPROVE_OPTION) {
+		if (event.getSource() == convert_xml_button1){
+
+		    xsl_file1 = chooser.getSelectedFile();
+		    convert_xml_field1.setText(xsl_file1.getPath());
+		}
+                else if (event.getSource() == convert_xml_button2){ 
+                     xsl_file2 = chooser.getSelectedFile();
+		    convert_xml_field2.setText(xsl_file2.getPath());
+		   
+		}
+		else {
+		    mapping_file = chooser.getSelectedFile();
+		    mapping_xml_field.setText(mapping_file.getPath()); 
+		}
+	    }
+	}
+     }
+
+ private class FolderButtonListener implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    JFileChooser chooser = new JFileChooser();
+	    chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+	   
+	    int returnVal = chooser.showOpenDialog(prompt);        
+	    
+	    if(returnVal == JFileChooser.APPROVE_OPTION) {
+		File folder_name = chooser.getSelectedFile();
+		title_field.setText(folder_name.getPath());
+	    }
+	    
+	}
+ }
+}
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FedoraLogin.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FedoraLogin.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FedoraLogin.java	(revision 31635)
@@ -0,0 +1,466 @@
+/*
+ * -----------------------------------------------------------------------------
+ *
+ * <p><b>License and Copyright: </b>The contents of this file are subject
+ * to the Educational Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy
+ * of the License at <a href="http://www.opensource.org/licenses/ecl1.txt">
+ * http://www.opensource.org/licenses/ecl1.txt.</a></p>
+ *
+ * <p>Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+ * License for the specific language governing rights and limitations under
+ * the License.</p>
+ *
+ * <p>The entire file consists of original code.  Copyright &copy;
+ * 2002-2007 by The Rector and Visitors of the University of Virginia and
+ * Cornell University.  All rights reserved.</p>
+ *
+ * -----------------------------------------------------------------------------
+ */
+
+
+/* Based on Fedora's Client LoginDialog.java code */
+/* Modified to work with Greenstone: 22 Jan 2008 */
+
+package org.greenstone.gatherer.gui;
+
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+
+import javax.swing.AbstractAction;
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import org.greenstone.gatherer.Dictionary;
+
+
+
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.Configuration;
+
+public class FedoraLogin extends JDialog 
+{
+    private static final int MAX_ITEMS = 5;
+
+    private static final java.awt.Color LIGHT = new java.awt.Color(244, 244, 224);
+    private static final java.awt.Color TRANSPARENT = new java.awt.Color(0,0,0,0);
+
+    private JPanel m_infoPane;
+    private JComboBox m_serverComboBox;
+    private JComboBox m_protocolComboBox;
+    private JComboBox m_usernameComboBox;
+    private JPasswordField m_passwordField;
+
+    private String m_lastUsername="fedoraAdmin";
+    private String m_lastServer="localhost:8080";
+    private String m_lastProtocol="http";
+    private String m_lastPassword="";
+
+    private boolean login_requested = false;
+
+    public FedoraLogin(String title, boolean can_cancel) 
+    {
+	super(Gatherer.g_man, "Login", true);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+        
+	JLabel serverLabel=new JLabel("Fedora Server");        
+	JLabel protocolLabel=new JLabel("Protocol");
+        JLabel usernameLabel=new JLabel("Username");
+        JLabel passwordLabel=new JLabel("Password");
+
+        serverLabel.setComponentOrientation(Dictionary.getOrientation());
+        protocolLabel.setComponentOrientation(Dictionary.getOrientation());
+        usernameLabel.setComponentOrientation(Dictionary.getOrientation());
+        passwordLabel.setComponentOrientation(Dictionary.getOrientation());
+        
+        m_serverComboBox=new JComboBox(new String[]{m_lastServer});
+        m_serverComboBox.setComponentOrientation(Dictionary.getOrientation());
+        m_serverComboBox.setEditable(true);
+	m_protocolComboBox=new JComboBox(new String[]{m_lastProtocol, "https"}); // http and https
+        m_protocolComboBox.setComponentOrientation(Dictionary.getOrientation());        
+	m_protocolComboBox.setEditable(true);
+        m_usernameComboBox=new JComboBox(new String[]{m_lastUsername});
+        m_usernameComboBox.setComponentOrientation(Dictionary.getOrientation());
+        m_usernameComboBox.setEditable(true);
+        m_passwordField=new JPasswordField();
+
+        setComboBoxValues();
+
+	
+        LoginAction loginAction=new LoginAction(this);
+        JButton loginButton=new JButton(loginAction);
+
+        loginAction.setButton(loginButton);
+        loginButton.setEnabled(false);
+	this.getRootPane().setDefaultButton(loginButton);
+
+        m_passwordField.getDocument().addDocumentListener(
+                new PasswordChangeListener(loginButton, m_passwordField));
+        m_passwordField.setAction(loginAction);
+
+        JPanel inputPane=new JPanel();
+        inputPane.setComponentOrientation(Dictionary.getOrientation());
+        inputPane.setBorder(BorderFactory.createCompoundBorder(
+                BorderFactory.createCompoundBorder(
+                    BorderFactory.createEmptyBorder(6, 6, 6, 6),
+                    BorderFactory.createEtchedBorder()
+                ),
+                BorderFactory.createEmptyBorder(6,6,6,6)
+                ));
+        GridBagLayout gridBag=new GridBagLayout();
+        inputPane.setLayout(gridBag);
+        addLabelValueRows(new JLabel[] {serverLabel, protocolLabel, usernameLabel, passwordLabel}, 
+                new JComponent[] {m_serverComboBox, m_protocolComboBox, m_usernameComboBox, m_passwordField}, 
+                gridBag, inputPane);
+	
+	// handling Closing and cancelling events
+	this.addWindowListener(new WindowAdapter() {
+		public void windowClosing(WindowEvent e){
+		    // this is important, if we want to stop looping on displaying the dialog:
+		    login_requested = false; 
+		    //dispose(); // defaultCloseOperation of this dialog already set to dispose it
+		}
+	    });
+
+        JButton cancelButton=new JButton(new AbstractAction() {
+        	private static final long serialVersionUID = 1L;
+            public void actionPerformed(ActionEvent evt) {
+		// this is important, if we want to stop looping on displaying the dialog:
+		login_requested = false;
+                dispose();
+            }
+        });
+
+	cancelButton.setText("Exit"); // if haven't logged in yet
+
+        JPanel buttonPane=new JPanel();
+        buttonPane.setComponentOrientation(Dictionary.getOrientation());
+        buttonPane.add(loginButton);
+        buttonPane.add(cancelButton);
+        Container contentPane=getContentPane();
+        contentPane.setComponentOrientation(Dictionary.getOrientation());
+
+        contentPane.setLayout(new BorderLayout());
+	m_infoPane = new JPanel();
+        m_infoPane.setComponentOrientation(Dictionary.getOrientation());
+	m_infoPane.setBackground(TRANSPARENT);
+	contentPane.add(m_infoPane, BorderLayout.NORTH);
+        contentPane.add(inputPane, BorderLayout.CENTER);
+        contentPane.add(buttonPane, BorderLayout.SOUTH);
+        addWindowListener(new WindowAdapter() {
+            public void windowOpened(WindowEvent evt) {
+                m_passwordField.requestFocus();
+            }
+        });
+        pack();
+
+	// Position
+	Dimension size = getSize();
+	if (Gatherer.g_man != null) {
+	    Rectangle frame_bounds = Gatherer.g_man.getBounds();
+	    setLocation(frame_bounds.x + (frame_bounds.width - size.width) / 2, frame_bounds.y + (frame_bounds.height - size.height) / 2);
+	}
+	else {
+	    Dimension screen_size = Toolkit.getDefaultToolkit().getScreenSize();
+	    setLocation((screen_size.width - size.width) / 2, (screen_size.height - size.height) / 2);
+	}
+
+
+        setVisible(true);
+    }
+
+
+    public void saveProperties(JComboBox control, String affectedProperty, String editedValue) {
+	//String editedValue = (String)control.getSelectedItem();
+	// Edited value is the value in the combobox editfield, it has not yet been added to the combobox
+	control.insertItemAt(editedValue, 0); 
+	
+	if(editedValue == null) {
+	    editedValue = (String)control.getItemAt(0); // else reuse the first default
+	} 
+	Configuration.setString(affectedProperty, true, editedValue);
+	
+	// Add the values in the combobox as well.
+	int value_count = 1;
+	for(int i = 0; value_count < MAX_ITEMS && i < control.getItemCount(); i++, value_count++) {
+	    String value = (String)control.getItemAt(i);
+	    if(value == null || value.equals(editedValue) || value.equals("")) { 
+		// skip duplicates of just-edited comboBox value and empty values
+		value_count--;
+	    } else { 
+		Configuration.setString(affectedProperty+value_count, true, value);
+	    } // else retain old value for this affectedProperty (e.g. general.gliserver_url)
+	}
+    }
+
+    private void setComboBoxValues() {
+        // All comboboxes are of the same size
+        Dimension newSize=new Dimension(m_serverComboBox.getPreferredSize().width+20, m_serverComboBox.getPreferredSize().height);
+	m_passwordField.setPreferredSize(newSize);
+      
+	setComboBoxValues(m_serverComboBox, "fedora.server", newSize);
+	setComboBoxValues(m_protocolComboBox, "fedora.protocol", newSize);
+	setComboBoxValues(m_usernameComboBox, "fedora.username", newSize);
+	newSize = null;
+    }
+    
+    private void setComboBoxValues(JComboBox control, String affectedProperty, Dimension newSize) {
+	// get values from Configuration file, if none, it will go back to using defaultvalues
+	// put them into the combobox
+	
+	String value = Configuration.getString(affectedProperty, true);
+	boolean finished = value.equals("");
+	if(!finished) {
+	    control.removeAllItems(); // remove defaults, since config file now contains init values
+	    control.addItem(value);
+	} // else value and combobox already contains the defaults
+	   	    
+	
+	for(int i = 1; !finished; i++) {
+	    value = Configuration.getString(affectedProperty+i, true);
+	    if(value.equals("")) {
+		finished = true;
+	    }
+	    else { // value is not empty
+		control.addItem(value);
+	    }
+	}
+	
+	// setting the size of the dropdown control
+	control.setPreferredSize(newSize);
+    }
+
+    public void addLabelValueRows(JLabel[] labels, JComponent[] values,
+				  GridBagLayout gridBag, Container container) {
+        GridBagConstraints c=new GridBagConstraints();
+        c.insets=new Insets(0, 6, 6, 6);
+        for (int i=0; i<labels.length; i++) {
+            c.anchor=GridBagConstraints.EAST;
+            c.gridwidth=GridBagConstraints.RELATIVE; //next-to-last
+            c.fill=GridBagConstraints.NONE;      //reset to default
+            c.weightx=0.0;                       //reset to default
+            gridBag.setConstraints(labels[i], c);
+            container.add(labels[i]);
+	    
+            c.gridwidth=GridBagConstraints.REMAINDER;     //end row
+            if (!(values[i] instanceof JComboBox)) {
+                c.fill=GridBagConstraints.HORIZONTAL;
+            } else {
+                c.anchor=GridBagConstraints.WEST;
+            }
+            c.weightx=1.0;
+            gridBag.setConstraints(values[i], c);
+            container.add(values[i]);
+        }
+	
+    }
+
+    /** Displays the given errormessage (which is split over several lines)
+	* in the FedoraLogin itself 
+	* @param errorLines: the lines of the error message, where each line
+	* will be presented in its own JLabel in the infoPane.
+     */
+    public void setErrorMessage(String[] errorLines) {
+	m_infoPane.removeAll();
+	m_infoPane.setBackground(LIGHT);
+	// n rows and 1 column
+	m_infoPane.setLayout(new java.awt.GridLayout(errorLines.length, 1));
+	for(int i = 0; i < errorLines.length; i++) {
+	    JLabel line = new JLabel("  " + errorLines[i] + "  ");
+	    m_infoPane.add(line);
+	}
+	
+	// Adjust and resize this dialog to take into account the 
+	// recently added components (labels)
+	m_infoPane.validate();	
+	this.pack();
+    }
+
+    public class PasswordChangeListener
+            implements DocumentListener {
+
+        private JButton m_loginButton;
+        private JPasswordField m_passField;
+
+        public PasswordChangeListener(JButton loginButton, JPasswordField pf) {
+            m_loginButton=loginButton;
+            m_passField=pf;
+        }
+
+        public void changedUpdate(DocumentEvent e) {
+            dataChanged();
+        }
+
+        public void insertUpdate(DocumentEvent e) {
+            dataChanged();
+        }
+
+        public void removeUpdate(DocumentEvent e) {
+            dataChanged();
+        }
+	
+        public void dataChanged() {
+	    if (m_passField.getPassword().length == 0) { 
+                m_loginButton.setEnabled(false);
+            } else {
+                m_loginButton.setEnabled(true);
+            }
+        }
+
+    }
+    
+    
+      public class LoginAction
+	  extends AbstractAction {
+
+	  FedoraLogin m_loginDialog;
+	  JButton m_button;
+	  
+	  public LoginAction(FedoraLogin loginDialog) {
+	      super("Login");
+	      m_loginDialog=loginDialog;
+	  }
+	  
+	  public void setButton(JButton button) {
+            m_button=button;
+	  }
+	  
+	  public void actionPerformed(ActionEvent evt) {
+	     
+	    
+            if (m_button.isEnabled()) {
+
+                try {
+                    // pull out values and do a quick syntax check
+                    String hostPort=(String) m_serverComboBox.getSelectedItem();
+                    int colonPos=hostPort.indexOf(":");
+                    if (colonPos==-1) {
+                        throw new IOException("Server must be specified as host:port");
+                    }
+                    String[] s=hostPort.split(":");
+                    String host=s[0];
+                    if (host.length()==0) {
+                        throw new IOException("No server name provided.");
+                    }
+                    int port=0;
+                    try {
+                        port=Integer.parseInt(s[1]);
+                    } catch (NumberFormatException nfe) {
+                        throw new IOException("Server port must be an integer.");
+                    }
+		    String protocol=(String) m_protocolComboBox.getSelectedItem();
+		    if (protocol.equals("")) {
+			throw new IOException("No protocol provided.");
+		    }
+
+                    String username=(String) m_usernameComboBox.getSelectedItem();
+                    if (username.equals("")) {
+                        throw new IOException("No username provided.");
+                    }
+                    String pass = new String(m_passwordField.getPassword());
+
+
+                    // all looks ok...just save stuff and exit now
+		    m_lastServer=host + ":" + port;
+                    m_lastProtocol=protocol;
+                    m_lastUsername=username;
+		    m_lastPassword=pass;
+
+		    m_loginDialog.saveProperties(m_serverComboBox, "fedora.server", m_lastServer);
+		    m_loginDialog.saveProperties(m_protocolComboBox, "fedora.protocol", m_lastProtocol);
+		    m_loginDialog.saveProperties(m_usernameComboBox, "fedora.username", m_lastUsername);
+		   
+		    // now save these values to the Configuration file
+		    Configuration.save();                   
+
+		    login_requested = true;
+
+		    // hiding it instead of disposing it on OK-press allows us to loop
+		    // on displaying the dialog in case there's more checking to do
+		    m_loginDialog.setVisible(false);
+
+                } catch (Exception e) {
+                    //String msg = e.getMessage();
+		    //System.err.println(msg);
+
+		    e.printStackTrace();
+
+                }
+            }
+	  }    
+      }
+
+    public boolean loginRequested()
+    {
+	return login_requested;
+    }
+
+    public String getLibraryURL()
+    {
+	String libraryUrl = m_lastProtocol + "://" + m_lastServer + "/fedora/search";
+
+	return libraryUrl;
+    }
+
+    public String getHostname()
+    {
+	String[] s=m_lastServer.split(":");
+	String host=s[0];
+	return host;
+    }
+    public String getPort()
+    {
+	String[] s=m_lastServer.split(":");
+
+	String port = s[1];
+	return port;
+    }
+
+    public String getUsername()
+    {
+	return m_lastUsername;
+    }
+
+    public String getPassword()
+    {
+	return m_lastPassword;
+    }
+
+    public String getProtocol()
+    {
+	return m_lastProtocol;
+    }
+
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FileAssociationDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FileAssociationDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FileAssociationDialog.java	(revision 31635)
@@ -0,0 +1,499 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.File;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.filechooser.*;
+import javax.swing.table.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.file.FileAssociationManager;
+import org.greenstone.gatherer.util.TableUtils;
+import org.greenstone.gatherer.util.Utility;
+
+/** The file association allows the entry of new file associations and the modification of existing ones.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class FileAssociationDialog
+    extends ModalDialog {
+
+    /** The default size for the dialog. */
+    static final private Dimension SIZE = new Dimension(600, 450);
+
+    private boolean ignore = false;
+    private FileAssociationDialog self;
+    private FileAssociationManager file_association_manager;
+    private JButton add_button;
+    private JButton browse_button;
+    private JButton close_button;
+    private JButton remove_button;
+    private JButton replace_button;
+    private JTable existing_associations_table;
+    private JTextField command_field;
+    private JTextField extension_field;
+
+    /** Create a new file association dialog.
+     * @param file_association_manager A reference to the <strong>FileAssociationManager</strong> so we can determine what extensions are already associated.
+     */
+    public FileAssociationDialog(FileAssociationManager file_association_manager) {
+	super(Gatherer.g_man);
+	this.file_association_manager = file_association_manager;
+	this.self = this;
+        this.setComponentOrientation(Dictionary.getOrientation());
+	// Creation
+	setModal(true);
+	setSize(SIZE);
+	setTitle(Dictionary.get("FileAssociationDialog.Title"));
+	setJMenuBar(new SimpleMenuBar("fileassociations"));
+
+	JPanel content_pane = (JPanel) getContentPane();
+	content_pane.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JTextArea instructions_area = new JTextArea(Dictionary.get("FileAssociationDialog.Instructions"));
+	instructions_area.setEditable(false);
+	instructions_area.setLineWrap(true);
+	instructions_area.setRows(5);
+	instructions_area.setWrapStyleWord(true);
+	instructions_area.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel control_pane = new JPanel();
+        control_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	existing_associations_table = new JTable(file_association_manager);
+	existing_associations_table.setComponentOrientation(Dictionary.getOrientation());
+        existing_associations_table.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN);
+	existing_associations_table.setColumnSelectionAllowed(false);
+	existing_associations_table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+	JPanel lower_pane = new JPanel();
+        lower_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel details_pane = new JPanel();
+        details_pane.setComponentOrientation(Dictionary.getOrientation());
+
+	JPanel extension_pane = new JPanel();
+        extension_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel extension_label = new JLabel(Dictionary.get("FileAssociationDialog.Extension"));
+        extension_label.setComponentOrientation(Dictionary.getOrientation());
+	extension_field = new NonWhitespaceField();
+	extension_field.setToolTipText(Dictionary.get("FileAssociationDialog.Extension_Tooltip"));
+                
+	JLabel command_label = new JLabel(Dictionary.get("FileAssociationDialog.Command"));
+        command_label.setComponentOrientation(Dictionary.getOrientation());
+        JPanel command_pane = new JPanel();
+        command_pane.setComponentOrientation(Dictionary.getOrientation());
+	command_field = new JTextField();
+        command_field.setComponentOrientation(Dictionary.getOrientation());
+	command_field.setToolTipText(Dictionary.get("FileAssociationDialog.Command_Tooltip"));
+	browse_button = new GLIButton(Dictionary.get("FileAssociationDialog.Browse"));
+	browse_button.setEnabled(!Utility.isMac());
+	if (Utility.isMac()) {
+	    browse_button.setToolTipText(Dictionary.get("FileAssociationDialog.Browse_Tooltip_Mac"));
+	} else {
+	    browse_button.setToolTipText(Dictionary.get("FileAssociationDialog.Browse_Tooltip"));
+	}
+
+
+	JPanel button_pane = new JPanel();       
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	add_button = new GLIButton(Dictionary.get("FileAssociationDialog.Add"), Dictionary.get("FileAssociationDialog.Add_Tooltip"));
+	add_button.setEnabled(false);
+	
+	replace_button = new GLIButton(Dictionary.get("FileAssociationDialog.Replace"), Dictionary.get("FileAssociationDialog.Replace_Tooltip"));
+	replace_button.setEnabled(false);
+	
+	remove_button = new GLIButton(Dictionary.get("FileAssociationDialog.Remove"), Dictionary.get("FileAssociationDialog.Remove_Tooltip"));
+	remove_button.setEnabled(false);
+	
+	close_button = new GLIButton(Dictionary.get("FileAssociationDialog.Close"), Dictionary.get("FileAssociationDialog.Close_Tooltip"));
+	
+	// Connection
+	add_button.addActionListener(new AddButtonListener());
+	browse_button.addActionListener(new BrowseButtonListener());
+	close_button.addActionListener(new CloseButtonListener());
+	remove_button.addActionListener(new RemoveButtonListener());
+	replace_button.addActionListener(new ReplaceButtonListener());
+	CommandOrExtensionFieldListener coefl = new CommandOrExtensionFieldListener();
+	command_field.getDocument().addDocumentListener(coefl);
+	extension_field.getDocument().addDocumentListener(coefl);
+	coefl = null;
+	existing_associations_table.getSelectionModel().addListSelectionListener(new ExistingAssociationsTableListener());
+
+	// Layout
+	extension_pane.setBorder(BorderFactory.createEmptyBorder(2,0,2,0));
+	extension_pane.setLayout(new BorderLayout(5,0));
+	extension_pane.add(extension_label, BorderLayout.LINE_START);
+	extension_pane.add(extension_field, BorderLayout.CENTER);
+
+	command_pane.setBorder(BorderFactory.createEmptyBorder(2,0,2,0));
+	command_pane.setLayout(new BorderLayout());
+	command_pane.add(command_field, BorderLayout.CENTER);
+	command_pane.add(browse_button, BorderLayout.LINE_END);
+
+	details_pane.setBorder(BorderFactory.createTitledBorder(Dictionary.get("FileAssociationDialog.Details")));
+	details_pane.setLayout(new GridLayout(3,1));
+	details_pane.add(extension_pane);
+	details_pane.add(command_label);
+	details_pane.add(command_pane);
+
+	lower_pane.setBorder(BorderFactory.createEmptyBorder(2,0,0,0));
+	button_pane.setLayout(new GridLayout(2,3));
+	button_pane.add(add_button);
+	button_pane.add(replace_button);
+	button_pane.add(remove_button);
+        JPanel tmp ;
+        tmp = new JPanel();
+        tmp.setComponentOrientation(Dictionary.getOrientation());
+	button_pane.add(tmp);
+        tmp = new JPanel();
+        tmp.setComponentOrientation(Dictionary.getOrientation());
+	button_pane.add(tmp);
+	button_pane.add(close_button);
+
+	lower_pane.setBorder(BorderFactory.createEmptyBorder(2,0,0,0));
+	lower_pane.setLayout(new BorderLayout());
+	lower_pane.add(details_pane, BorderLayout.CENTER);
+	lower_pane.add(button_pane, BorderLayout.SOUTH);
+
+	control_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	control_pane.setLayout(new BorderLayout());
+        JScrollPane scrol_tmp;
+        scrol_tmp = new JScrollPane(existing_associations_table);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	control_pane.add(scrol_tmp, BorderLayout.CENTER);
+	control_pane.add(lower_pane, BorderLayout.SOUTH);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+        scrol_tmp = new JScrollPane(instructions_area);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.add(scrol_tmp, BorderLayout.NORTH);
+	content_pane.add(control_pane, BorderLayout.CENTER);
+
+	Rectangle screen = Gatherer.g_man.getBounds(null);
+	setLocation((int)(screen.getX() + (screen.getWidth() - SIZE.width) / 2), (int)(screen.getY() + (screen.getHeight() - SIZE.height) / 2));
+	screen = null;
+        
+    }
+
+    public void destroy() {
+	// Disconnect
+	// Clean up
+	file_association_manager = null;
+	self = null;
+    }
+
+    /** Redisplay the dialog, ensuring that the details (or lack thereof) for a certain extension is apparent. In fact if an extension is provided force the user to add it or cancel.
+     * @param new_extension The extension code as a <strong>String</strong>.
+     */
+    public String display(String new_extension) {
+	// Clear the current selection
+	existing_associations_table.clearSelection();
+	if(new_extension != null) {
+	    extension_field.setText(new_extension);
+	}
+	setVisible(true);
+	if(new_extension != null) {
+	    return file_association_manager.getCommandString(new_extension);
+	}
+	return null;
+    }
+
+    protected void processWindowEvent(WindowEvent event) {
+	if(event.getID() == WindowEvent.WINDOW_ACTIVATED) {
+	    TableUtils.fixColumnToPreferredWidth(existing_associations_table, 0); 
+	}
+	else {
+	    super.processWindowEvent(event);
+	}
+    }
+
+    private class AddButtonListener
+	implements ActionListener {
+	
+	public void actionPerformed(ActionEvent event) {
+	    String extension_str = extension_field.getText();
+	    file_association_manager.setCommand(extension_str, command_field.getText());
+	    // Select the appropriate entry in the table
+	    int index = -1;
+	    for(int i = 0; index == -1 && i < file_association_manager.size(); i++) {
+		if(file_association_manager.getExtension(i).equals(extension_str)) {
+		    index = i;
+		}
+	    }
+	    if(index > -1) {
+		existing_associations_table.setRowSelectionInterval(index, index);
+	    }
+	    // And update buttons
+	    add_button.setEnabled(false);
+	    remove_button.setEnabled(true);
+	}
+    }
+
+    /** Batch filter shows only files ending in bat. Based on ImageFilter.java which is a 1.4 example used by FileChooserDemo2.java. */
+    private class BatchFileFilter
+	extends FileFilter {
+
+	/** Accept all .exe files
+	 * @param file a File
+	 * @return true is this file should be shown, false otherwise
+	 */
+	public boolean accept(File file) {
+	    return file.getName().toLowerCase().endsWith(".bat");
+	}
+
+	/** The description of this filter
+	 * @return a String
+	 */
+	public String getDescription() {
+	    return Dictionary.get("FileAssociationDialog.Batch_File");
+	}
+    }
+
+    /** Whenever the user clicks the browse button, we should open up a file browser to allow them to select an executable file from somewhere in the file system. */
+    private class BrowseButtonListener 
+	implements ActionListener {
+	/** Open up a simple JFileChooser when the user clicks the button. 
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    JFileChooser chooser = new JFileChooser(new File(Gatherer.getGLIUserDirectoryPath()));
+	    GUIUtils.disableRename(chooser);
+	    chooser.setDialogTitle(Dictionary.get("FileAssociationDialog.Browse_Title"));
+	    chooser.setFileFilter(new BatchFileFilter());
+	    chooser.setFileFilter(new CoreObjectModelFileFilter());
+	    chooser.setFileFilter(new ExecutableFileFilter());
+	    chooser.setAcceptAllFileFilterUsed(true);
+	    if(chooser.showOpenDialog(Gatherer.g_man) == JFileChooser.APPROVE_OPTION) {
+		command_field.setText(chooser.getSelectedFile().getAbsolutePath());
+	    }
+	}
+    }
+
+    private class CloseButtonListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    existing_associations_table.clearSelection();
+	    add_button.setEnabled(false);
+	    remove_button.setEnabled(false);
+	    replace_button.setEnabled(false);
+	    self.dispose();
+	}
+    }
+
+    /** Command filter shows only files ending in com. Based on ImageFilter.java which is a 1.4 example used by FileChooserDemo2.java. */
+    private class CoreObjectModelFileFilter
+	extends FileFilter {
+
+	/** Accept all .exe files
+	 * @param file a File
+	 * @return true is this file should be shown, false otherwise
+	 */
+	public boolean accept(File file) {
+	    return file.getName().toLowerCase().endsWith(".com");
+	}
+
+	/** The description of this filter
+	 * @return a String
+	 */
+	public String getDescription() {
+	    return Dictionary.get("FileAssociationDialog.Command_File");
+	}
+    }
+
+    private class CommandOrExtensionFieldListener
+	implements DocumentListener {
+	/** Gives notification that an attribute or set of attributes changed. */
+	public void changedUpdate(DocumentEvent e) {
+	    changed();
+	}
+	/** Gives notification that there was an insert into the document. */
+	public void insertUpdate(DocumentEvent e) {
+	    changed();
+	}
+	/** Gives notification that a portion of the document has been removed. */
+	public void removeUpdate(DocumentEvent e) {
+	    changed();
+	} 
+
+	private void changed() {
+	    ignore = true;
+	    String extension_str = extension_field.getText();
+	    String command_str = command_field.getText();
+	    // Determine if there is currently an association selected
+	    int selected_index = -1;
+	    if((selected_index = existing_associations_table.getSelectionModel().getMinSelectionIndex()) != -1) {
+		String current_extension_str = file_association_manager.getExtension(selected_index);
+		String current_command_str = file_association_manager.getCommandString(current_extension_str);
+		// What buttons are enabled depends a lot on the file extension, taken to be the 'key'
+		if(extension_str.equals(current_extension_str)) {
+		    // If the extensions are the same then remove is enabled
+		    remove_button.setEnabled(true);
+		    // But if the command is different, then enable replace
+		    if(current_command_str.length() == 0) {
+			add_button.setEnabled(true);
+			replace_button.setEnabled(false);
+		    }
+		    else if(!command_str.equals(current_command_str)) {
+			add_button.setEnabled(false);
+			replace_button.setEnabled(true);
+		    }
+		    else {
+			add_button.setEnabled(false);
+			replace_button.setEnabled(false);
+		    }
+		}
+		// We can add it, but we first clear the selection
+		else {
+		    existing_associations_table.clearSelection();
+		    add_button.setEnabled(true);
+		    remove_button.setEnabled(false);
+		    replace_button.setEnabled(false);
+		}
+	    }
+	    // If not, we must first test if the extension is in use
+	    else {
+		int index = -1;
+		for(int i = 0; index == -1 && i < file_association_manager.size(); i++) {
+		    String existing_extension_str = file_association_manager.getExtension(i);
+		    if(existing_extension_str.equals(extension_str)) {
+			index = i;
+		    }
+		}
+		if(index != -1) {
+		    // Selection that index
+		    existing_associations_table.setRowSelectionInterval(index, index);
+		    // Set replace and remove enabled
+		    add_button.setEnabled(false);
+		    remove_button.setEnabled(true);
+		    replace_button.setEnabled(true);
+		}
+		// Otherwise if there is some text in both extension and command, enable add only
+		else if(extension_str.length() > 0 && command_str.length() > 0) {
+		    add_button.setEnabled(true);
+		    remove_button.setEnabled(false);
+		    replace_button.setEnabled(false);
+		}
+		// Otherwise disable everything
+		else {
+		    add_button.setEnabled(false);
+		    remove_button.setEnabled(false);
+		    replace_button.setEnabled(false);
+		}
+	    }
+	    ignore = false;
+	}
+    }
+
+    /** Executable filter shows only files ending in exe. Based on ImageFilter.java which is a 1.4 example used by FileChooserDemo2.java. */
+    private class ExecutableFileFilter
+	extends FileFilter {
+
+	/** Accept all .exe files
+	 * @param file a File
+	 * @return true is this file should be shown, false otherwise
+	 */
+	public boolean accept(File file) {
+	    return file.getName().toLowerCase().endsWith(".exe");
+	}
+
+	/** The description of this filter
+	 * @return a String
+	 */
+	public String getDescription() {
+	    return Dictionary.get("FileAssociationDialog.Executable_File");
+	}
+    }
+
+    private class ExistingAssociationsTableListener
+	implements ListSelectionListener {
+
+	public void valueChanged(ListSelectionEvent event) {
+	    if(!event.getValueIsAdjusting() && !ignore) {
+		int selected_index = -1;
+		if((selected_index = existing_associations_table.getSelectionModel().getMinSelectionIndex()) != -1) {
+		    // Propagate the details of the selection down into the fields
+		    String extension_str = file_association_manager.getExtension(selected_index);
+		    extension_field.setText(extension_str);
+		    command_field.setText(file_association_manager.getCommandString(extension_str));
+		    extension_str = null;
+		    remove_button.setEnabled(true);
+		}
+		else {
+		    // Clear the fields
+		    extension_field.setText("");
+		    command_field.setText("");
+		    remove_button.setEnabled(false);
+		}
+		add_button.setEnabled(false);
+		replace_button.setEnabled(false);
+	    }
+	}
+    }
+
+    private class RemoveButtonListener 
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    file_association_manager.setCommand(extension_field.getText(), null);
+	    // And update buttons
+	    add_button.setEnabled(true);
+	    remove_button.setEnabled(false);
+	}
+    }
+
+    private class ReplaceButtonListener 
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    file_association_manager.setCommand(extension_field.getText(), command_field.getText());
+	    // And update buttons
+	    replace_button.setEnabled(false);
+	}
+    }
+}
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/Filter.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/Filter.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/Filter.java	(revision 31635)
@@ -0,0 +1,336 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import java.util.regex.*;
+import javax.swing.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.file.FileSystemModel;
+import org.greenstone.gatherer.gui.tree.DragTree;
+
+/** <p>This object allows the user to set a filter on one of the workspace trees, specifying a preset type, or a regular expression that a files must match to be in the tree. Note that all directories are included. This class includes the controls for editing the filter. The trick is that several instances of the Filter class can share the same internal data (termed a 'run' of filters), so that the filter set on the GatherPane and the EnrichPane are virtually the same.</p>
+ * <p>The regular expression typed uses '*' as a wildcard character (equivalent to '.*'), and does not use '.' to match any single character (use '?' instead).</p>
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class Filter
+    extends JPanel {
+    /** The other filters in this run of filters, used to ensure they all show the same thing. */
+    private ArrayList others = null;
+    /** Is this the first filter of this run of filters created (later filters will share the same information). */
+    private boolean first = true;
+    /** Prevent any changes we make in the class from causing events which we then process causing events... */
+    private boolean ignore = false;
+    /** A reference to ourselves so inner classes can refer to us. */
+    private Filter this_filter = null;
+    /** The check box to enable/disable filter. */
+    private JCheckBox checkbox = null;
+    /** The editable combobox where you either choose a predefined filter, or type a new pseudo-regular expression. */
+    private GComboBox combobox = null;
+    /** The label shown on the filter controls. */
+    private JLabel label = null;
+    /** A reference to the tree this filter is being applied to. */
+    private DragTree tree = null;
+    /** The default size for the label. */
+    static final private Dimension SIZE = new Dimension(100,30);
+    /** Preprogrammed default filters. */
+    static final private String DEFAULTS[] 
+	= {"^.*\\.html?$", "^.*\\.xml$", "^.*\\.txt$", 
+	   "(^.*\\.jpe?g$)|(^.*\\.png$)|(^.*\\.gif$)|(^.*\\.bmp$)|(^.*\\.tiff?$)",
+	   "^.*\\.pdf$", 
+	   "(^.*\\.docx?$)|(^.*\\.pptx?$)|(^.*\\.xlsx?$)|(^.*\\.od(t|s|p)$)"};
+
+    /** Constructor.
+     * @param tree A reference to the <strong>JTree</strong> being affected.
+     */
+    public Filter(DragTree tree) {
+	this(tree, null);
+    }
+
+    /** Constructor.
+     * @param tree A reference to the <strong>JTree</strong> being affected.
+     * @param others An <strong>ArrayList</strong> of the other Filters already in this run.
+     */
+    public Filter(DragTree tree, ArrayList others) {
+	super();
+        this.setComponentOrientation(Dictionary.getOrientation());
+	if (others == null) {
+	    others = new ArrayList();
+	}
+	this.others = others;
+	this.others.add(this);
+	this.this_filter = this;
+	this.tree = tree;
+	// Create components.
+	combobox = new GComboBox(true);
+	try {
+	    combobox.add(new Entry());
+	}
+	catch (Exception error) {
+	    error.printStackTrace();
+	}
+	for(int i = 0; i < DEFAULTS.length; i++) {
+	    try {
+		Entry entry = new Entry(Dictionary.get("Filter." + i), Pattern.compile(DEFAULTS[i]));
+		combobox.add(entry);
+	    }
+	    catch (Exception error) {
+		error.printStackTrace();
+	    }
+	}
+	label = new JLabel(Dictionary.get("Filter.Filter_Tree"));
+        label.setComponentOrientation(Dictionary.getOrientation());
+        label.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	combobox.setToolTipText(Dictionary.get("Collection.Filter_Tooltip"));
+	
+	// Add listeners.
+	combobox.addActionListener(new ComboBoxListener());
+	// Layout.
+	label.setBorder(BorderFactory.createEmptyBorder(0,0,0,5));
+
+	setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
+	setLayout(new BorderLayout());
+	add(label, BorderLayout.LINE_START);
+	add(combobox, BorderLayout.CENTER);
+    }
+
+    /** Used to restore the filter state to enabled, the normal state during collection editing.
+     * @param state The new state for the filter. <i>true</i> for enabled, <i>false</i> otherwise.
+     */
+    public void setEnabled(boolean state) {
+	ignore = true;
+	combobox.setEnabled(state);
+	ignore = false;
+    }
+
+    /** Set the combobox model for this filter.
+     * @param model The new <strong>ComboBoxModel</strong> to use.
+     */
+    public void setComboBoxModel(ComboBoxModel model) {
+	combobox.setModel(model);
+    }
+
+    /** Ensure that a certain entry is selected from the combobox.
+     * @param selection The <strong>Entry</strong> that should be selected.
+     */
+    public void setComboBoxSelection(Entry selection) {
+	ignore = true;
+	combobox.setSelectedItem(selection);
+	ignore = false;
+    }
+
+    /** Sets whether this combobox is editable or not
+     * @param value true to make the control editable, false otherwise
+     */
+    public void setEditable(boolean value) {
+	combobox.setEditable(value);	    
+    }
+
+    /** Set to signify that this filter is the first in a new run of filters.
+     * @param first <i>true</i> if this is the first filter in a run, <i>false</i> if it will just be added to the current run.
+     */
+    public void setFirst(boolean first) {
+	this.first = first;
+    }
+
+    /** Encode an expression in pseudo-regular expression into regular expression.
+     * @param raw The pseudo-regular expression <strong>String</strong> which includes several characters which differ in meaning from regular expression queries.
+     * @return A proper regular expression as a <strong>String</strong>.
+     */
+    private String encode(String raw) {
+	StringBuffer working = new StringBuffer();
+	for(int i = 0; i < raw.length(); i++) {
+	    char c = raw.charAt(i);
+	    switch(c) {
+	    case '.':
+		working.append("\\.");
+		break;
+	    case '*':
+		working.append(".*");
+		break;
+	    case '?':
+		working.append(".");
+		break;
+	    default:
+		working.append(Character.toLowerCase(c));
+	    }
+	}
+	return working.toString();
+    }
+    /** This method applies the given pattern to the tree registered as belonging to this filter.*/
+    private void setFilter(Pattern pattern) {
+	// Show busy cursor.
+	Gatherer.g_man.wait(true);
+	FileSystemModel model = (FileSystemModel) tree.getModel();
+	// Apply filter
+	if(pattern != null) {
+	    model.setFilter(pattern.pattern());
+	}
+	else {
+	    model.setFilter(null);
+	}
+	// Ask tree to completely refresh
+	tree.refresh(null);
+	// Restore cursor
+	Gatherer.g_man.wait(false);
+    }
+
+    /** Listens for changes in the combobox as when one is detected attempts to compile a regular expression from whatever text was entered. If successful, or if the item chosen was a predefined filter, it then applies the filter to the target tree. */
+    private class ComboBoxListener
+	implements ActionListener {
+	/** Called when a new item is selected from the filter combobox, we treat the new entry as a pseudo-regular expression, compile it and then apply it to the tree.
+	 * @param event An <strong>ActionEvent</strong> containing more information about the change performed.
+	 * @see org.greenstone.gatherer.gui.Filter.Entry
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    try {
+		Object temp = combobox.getSelectedItem();
+		Entry entry = null;
+		if(temp instanceof String) {
+		    String temp_str = (String) temp;
+		    ///ystem.err.println("Filter = " + temp_str);
+
+		    // Ignore any string which matches a predefined filter
+		    if(temp_str.equals(Dictionary.get("Filter.All_Files"))) {
+		    }
+		    // HTM & HTML
+		    else if(temp_str.equals(Dictionary.get("Filter.0"))) {
+		    }
+		    // XML
+		    else if(temp_str.equals(Dictionary.get("Filter.1"))) {
+		    }
+		    // Text files
+		    else if(temp_str.equals(Dictionary.get("Filter.2"))) {
+		    }
+		    // Images
+		    else if(temp_str.equals(Dictionary.get("Filter.3"))) {
+		    }
+		    // PDF
+		    else if(temp_str.equals(Dictionary.get("Filter.4"))) {
+		    }
+		    // Office Docs
+		    else if(temp_str.equals(Dictionary.get("Filter.5"))) {
+		    }
+		    else {
+			// Make sure the filter isn't already in the list
+			boolean already_exists = false;
+			for (int i = 0; i < combobox.getItemCount(); i++) {
+			    if (temp_str.equals(combobox.getItemAt(i).toString())) {
+				already_exists = true;
+				entry = (Entry) combobox.getItemAt(i);
+				break;
+			    }
+			}
+
+			if (already_exists == false) {
+			    entry = new Entry(temp_str, Pattern.compile(encode(temp_str)));
+			    int position = combobox.getItemCount();
+			    combobox.insertItemAt(entry, position);
+			    combobox.setSelectedIndex(position);
+			}
+		    }
+		}
+		else {
+		    ///ystem.err.println("Custom Filter");
+		    entry = (Entry) temp;
+		}
+		if(entry != null) {
+		    setFilter(entry.getPattern());
+		}
+		// Else we ignore this event as being one of the painfully erroneous events we receive because we've been silly enough to have an editable combobox.
+	    }
+	    catch (PatternSyntaxException error) {
+		if(first) {
+		    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("Filter.Invalid_Pattern"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		}
+	    }
+	}
+    }
+    /** An object that holds a filter entry. This is string used for the filter pattern and, if not custom built, its name. */
+    private class Entry
+	implements Comparable {
+	/** The compiled pattern created from a regular expression. */
+	private Pattern pattern = null;
+	/** The name of this filter entry. */
+	private String name = null;
+	/** Constructor. */
+	public Entry() {
+	}
+	/** Constructor.
+	 * @param name The name of this entry as a <strong>String</strong>.
+	 * @param pattern The compiled regular expression as a <strong>Pattern</strong>.
+	 */
+	public Entry(String name, Pattern pattern) {
+	    this.name = name;
+	    this.pattern = pattern;
+	}
+	/** Compare two Entrys for ordering.
+	 * @param object The other Entry to compare to, as an <strong>Object</strong>.
+	 * @return An <i>int</i> indicating the respective ordering, as defined in java.lang.String#compareTo
+	 */
+	public int compareTo(Object object) {
+	    return toString().compareTo(object.toString());
+	}
+	/** Retrieve the pattern associated with this entry.
+	 * @return The <strong>Pattern</strong>.
+	 */
+	public Pattern getPattern() {
+	    return pattern;
+	}
+	/** Translate this entry into a textual representation.
+	 * @return A <strong>String</strong> containing the representation.
+	 */
+	public String toString() {
+	    String result = null;
+	    if (name != null) {
+		result = name;
+	    }
+	    else if (pattern == null) {
+		result = Dictionary.get("Filter.All_Files");
+	    }
+	    else {
+		result = pattern.pattern();
+	    }
+	    return result;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FormatConversionDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FormatConversionDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FormatConversionDialog.java	(revision 31635)
@@ -0,0 +1,908 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.gui;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.CollectionConfigXMLReadWrite;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.SafeProcess;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+
+import org.w3c.dom.*;
+
+import java.io.File;
+import java.io.IOException;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import javax.swing.event.*;
+
+
+// TO DO:
+// + StreamGobblers: http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html
+// Need to test on Windows: the Ctrl-D/Ctrl-Z sent to formatconverter.exe. May not need to do anything special for Windows now that this code is using StreamGobblers to process the in/out streams of the process
+// + Need to use dictionary for labels
+// X convertFormat() for remote GS. Remote does not display Format Conversion wizard
+// + HTML tidy (put all "<br/>" back to "<br>" in luce/etc/collectionConfig.xml and run again)
+// + Help tooltips on buttons
+// + Undo, Redo buttons
+// X Split class into dialog/widgets and data processing?
+// http://www.java2s.com/Tutorial/Java/0240__Swing/SettingJOptionPanebuttonlabelstoFrench.htm
+
+public class FormatConversionDialog extends ModalDialog
+{
+    public static final String GSF_FORMAT_GS2_TAG = "gsf:format-gs2";
+    private static final String GSF_GS3_ROOT_TAG = "gsf:gs3-root";
+
+    // The cmdline programs launched by this dialog
+    private static final int XMLTIDY = 0;
+    private static final int FORMATCONVERTER = 1;
+    private static final int FORMATCONVERTER_DOCUMENTNODE = 2;
+    private static final int FORMATCONVERTER_CLASSIFIERNODE = 3;
+
+    // Online HTML tidy for learning usage: http://infohound.net/tidy/
+    private static final String[] xmltidy_cmd_args = {"tidy", "-config", Configuration.getGS3BinPath() + "tidyconfig.cfg", "-utf8", "-wrap", "0", "-raw", "-q"}; // {"tidy", "-xml", "-utf8"};
+    private static final String[] formatconverter_cmd_base_args = {Configuration.getGS3BinOSPath() + "formatconverter", "--silent"};
+
+    private static final Dimension SIZE = new Dimension(640,480);
+
+    // the important member variables
+    private File collect_cfg_file = null;
+    private Document xml_file_doc = null;
+    private NodeList gsf_format_gs2_list = null;    
+
+    private int current_index = -1;
+    private int dlgResult = OpenCollectionDialog.OK_OPTION;
+    private int process_exitValue = -1;
+    
+    // widgets
+    final String WARNING_TITLE = Dictionary.get("General.Warning") 
+	+ ": " + Dictionary.get("FormatConversionDialog.Invalid_XML")
+	+ " " + Dictionary.get("FormatConversionDialog.Invalid_XML_Warning_Title");
+
+    private static final Border EMPTYBORDER = new EmptyBorder(5, 5, 5, 5);
+    private JLabel section_label = null;
+    private GLIButton cancel_button = null;
+    private GLIButton next_button = null;
+    private GLIButton accept_all_button = null;
+    private GLIButton htmltidy_button = null;
+    private GLIButton xmltidy_button = null;
+    // the 2 NumberedJTextAreas. Each provide their own undo and redo buttons already hooked up to listeners
+    private NumberedJTextArea gs2_textarea = null;
+    private NumberedJTextArea gs3_textarea = null;
+    private JLabel count_label = null;
+    private JLabel statusbar = null;
+
+    public FormatConversionDialog (File collect_cfg_file, Document xml_file_doc, NodeList gsf_format_gs2_list) {
+	super(Gatherer.g_man, "", true);
+
+	this.collect_cfg_file = collect_cfg_file;	
+	this.xml_file_doc = xml_file_doc;
+	this.gsf_format_gs2_list = gsf_format_gs2_list;
+
+	this.setComponentOrientation(Dictionary.getOrientation());
+	setSize(SIZE);
+	setTitle(Dictionary.get("FormatConversionDialog.Title"));
+	this.addWindowListener(new WindowClosingListener()); // the dialog's top right close button should behave as a Cancel press
+	setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+	
+	// The NumberedJTextArea provides its own undo and redo buttons, already hooked up to listeners
+	// Just need to place these buttons in the dialog
+	gs2_textarea = new NumberedJTextArea("gs2", Dictionary.get("FormatConversionDialog.GS2_Text_Tooltip"));
+	gs3_textarea = new NumberedJTextArea("gs3", Dictionary.get("FormatConversionDialog.GS3_Text_Tooltip"));
+
+
+	JPanel midbutton_panel = new JPanel(); // FlowLayout by default in a JPanel
+	midbutton_panel.setComponentOrientation(Dictionary.getOrientation());
+
+	JButton reconvert_button = new GLIButton(Dictionary.get("FormatConversionDialog.Reconvert"), Dictionary.get("FormatConversionDialog.Reconvert_Tooltip"));
+
+	midbutton_panel.add(gs2_textarea.undoButton);
+	midbutton_panel.add(gs2_textarea.redoButton);
+	midbutton_panel.add(reconvert_button);
+	reconvert_button.addActionListener(new ReconvertListener());
+	reconvert_button.setAlignmentY(Component.CENTER_ALIGNMENT);
+
+	section_label = new JLabel("Section Label Goes Here"); // title will be replaced, don't need to translate
+	section_label.setBorder(EMPTYBORDER);
+	section_label.setComponentOrientation(Dictionary.getOrientation());
+
+	JPanel button1_panel = new JPanel();
+	button1_panel.setComponentOrientation(Dictionary.getOrientation());
+
+	xmltidy_button = new GLIButton(Dictionary.get("FormatConversionDialog.XHTML_Tidy"), Dictionary.get("FormatConversionDialog.XHTML_Tidy_Tooltip"));
+	xmltidy_button.addActionListener(new XMLTidyButtonListener());
+
+	button1_panel.add(gs3_textarea.undoButton);
+	button1_panel.add(gs3_textarea.redoButton);
+	button1_panel.add(xmltidy_button);
+
+	JPanel button_panel = new JPanel(new FlowLayout());
+	button_panel.setComponentOrientation(Dictionary.getOrientation());
+	count_label = new JLabel("<count>"); // placeholder text for numbers, no need to translate
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("FormatConversionDialog.Cancel_Tooltip"));
+	next_button = new GLIButton(Dictionary.get("FormatConversionDialog.Next"), Dictionary.get("FormatConversionDialog.Next_Tooltip"));
+	accept_all_button = new GLIButton(Dictionary.get("FormatConversionDialog.Accept_All"), Dictionary.get("FormatConversionDialog.Accept_All_Tooltip"));
+	cancel_button.addActionListener(new CancelButtonListener());
+	next_button.addActionListener(new NextButtonListener());
+	accept_all_button.addActionListener(new AcceptAllButtonListener());
+	button_panel.add(cancel_button);
+	button_panel.add(next_button);
+	button_panel.add(accept_all_button);
+	button_panel.add(count_label);
+
+
+	JPanel centre_panel = new JPanel();
+	centre_panel.setLayout(new BoxLayout(centre_panel, BoxLayout.Y_AXIS));
+	centre_panel.setComponentOrientation(Dictionary.getOrientation());
+	centre_panel.setBorder(EMPTYBORDER);
+	centre_panel.add(new JScrollPane(gs2_textarea));
+	centre_panel.add(midbutton_panel);
+	centre_panel.add(new JScrollPane(gs3_textarea));
+
+	JPanel bottom_panel = new JPanel();
+	bottom_panel.setLayout(new BoxLayout(bottom_panel, BoxLayout.Y_AXIS));
+	bottom_panel.setComponentOrientation(Dictionary.getOrientation());
+	bottom_panel.setBorder(EMPTYBORDER);
+
+	bottom_panel.add(button1_panel);
+	bottom_panel.add(button_panel);
+	statusbar = new JLabel("");
+	statusbar.setBorder(EMPTYBORDER);
+	// http://stackoverflow.com/questions/2560784/how-to-center-elements-in-the-boxlayout-using-center-of-the-element
+	statusbar.setAlignmentX(Component.CENTER_ALIGNMENT);
+	bottom_panel.add(statusbar);
+
+	// add all the widgets to the contentpane
+	JPanel content_pane = (JPanel) getContentPane();
+	content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setLayout(new BorderLayout());
+	//content_pane.setBorder(EMPTYBORDER);
+
+	content_pane.add(section_label, BorderLayout.NORTH);
+	content_pane.add(centre_panel, BorderLayout.CENTER);
+	content_pane.add(bottom_panel, BorderLayout.SOUTH);
+	
+
+	// Final dialog setup & positioning.
+	getRootPane().setDefaultButton(next_button);
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	screen_size = null;
+	//setVisible(true);
+    }
+
+
+    public int getDialogResult() {
+	return dlgResult; // OK_OPTION or CANCEL_OPTION
+    }
+    
+    //*************************PROCESSING FUNCTIONS***************************//
+    public static int checkForGS2FormatStatements(File collect_cfg_file) {
+
+	if(Gatherer.GS3 && collect_cfg_file.getAbsolutePath().endsWith(".xml")) {
+
+	    //System.err.println("*** Opening an xml config file");
+		
+	    Document xml_file_doc = XMLTools.parseXMLFile(collect_cfg_file);
+	    Element root = xml_file_doc.getDocumentElement();
+
+	    // check if there are any <gsf:format-gs2 /> elements. If there are, then may need to process them
+	    
+	    //NodeList gsf_format_gs2_list = root.getElementsByTagNameNS("gsf", "format-gs2");
+	    NodeList gsf_format_gs2_list = root.getElementsByTagName(FormatConversionDialog.GSF_FORMAT_GS2_TAG);
+	    if(gsf_format_gs2_list != null && gsf_format_gs2_list.getLength() > 0) {
+
+		// Sample the first of the <gsf:gs2-format/> elements to
+		// check we don't have any CDataSections in the <gsf:gs2-format/> elements
+		// If the first <gsf:gs2-format/> has a CDataSection, it means we've already 
+		// converted it to GS3 format statements during an earlier GLI session.
+
+		Node gs2format = gsf_format_gs2_list.item(0);
+		//gs2format.normalize();
+		NodeList children = gs2format.getChildNodes();
+
+		for(int i = 0; i < children.getLength(); i++) {
+		    Node child = children.item(i);
+		    if(child.getNodeType() == Node.CDATA_SECTION_NODE) {	
+			// there are GS2 statements in col config, but they've already been converted to GS3
+			// can open the collection without going through the FormatConversionDialog
+			return OpenCollectionDialog.OK_OPTION; 
+		    }
+		}
+		System.err.println("*** Found GS2 format statements in config file to be converted to GS3.");
+
+		
+		// if remote GS3, do we open the collection with the html-encoded GS2 format statements
+		// or do we not allow the remote user to open such a collection at all?
+		// For now we allow them to open it, but print a warning that conversions are not possible.
+		
+		if(Gatherer.isGsdlRemote) { // remote GS3
+		    System.err.println("*** Cannot convert GS2 collections from a remote GS3 server.");	
+		    return OpenCollectionDialog.OK_OPTION;
+		}
+
+		// If we get here, it means there were no CDataSections in the first (any) <gsf:gs2-format/>
+		// This means it's the first time the GS2 collection is being opened in GLI 
+		// Open the FormatCconversionDialog and convert the gs2 statements to gs3:
+		FormatConversionDialog formatconversionDlg = new FormatConversionDialog(collect_cfg_file, xml_file_doc, gsf_format_gs2_list);
+		formatconversionDlg.convertGS2FormatStatements();
+		return formatconversionDlg.getDialogResult();
+		
+	    }
+	}	
+	return OpenCollectionDialog.OK_OPTION; // no GS2 statements in col config, and can open the collection
+    }
+
+
+    /** 
+     * runInteractiveProgram() runs a cmdline program that reads from stdinput 
+     * until Ctrl-D is encountered. It outputs the result to stdout.
+     *
+     * The cmdline programs HTML Tidy and FormatConverter both behave the same way:
+     * When the formatconverter binary is run in silent mode, it expects input
+     * followed by a newline and then EOF (or if no newline, then 2 EOFs) 
+     * which is Ctrl-D on Linux/Mac and Ctrl-Z on Windows. 
+     * Then the cmdline program exits by printing the result of the conversion.
+     *
+     * Explicitly sending EOFs from java is no longer necessary, as the OutputStreamGobbler 
+     * Thread takes care of closing the process stream.
+     *
+     * HTMLTidy returns 0 if no warnings or errors, 1 if just Warnings, 2 if Errors (failure)
+     * http://sourceforge.net/p/tidy/mailman/tidy-develop/thread/20020811204058.GD1137@shaw.ca/
+     *
+     * This code uses the StreamGobbler classes based on
+     * http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
+    */    
+    public String runInteractiveProgram(int program, String inputstr) {
+	String outputstr = "";
+	String[] command_args;
+	process_exitValue = -1;
+
+	switch(program) {
+	case XMLTIDY:
+	    command_args = xmltidy_cmd_args;
+	    break;
+	case FORMATCONVERTER:
+	    command_args = formatconverter_cmd_base_args;
+	    break;
+	case FORMATCONVERTER_DOCUMENTNODE: 
+	case FORMATCONVERTER_CLASSIFIERNODE:
+	    command_args = new String[formatconverter_cmd_base_args.length+1]; 
+	    System.arraycopy(formatconverter_cmd_base_args, 0, command_args, 0, formatconverter_cmd_base_args.length);
+	    if(program == FORMATCONVERTER_DOCUMENTNODE) {
+		command_args[command_args.length-1] = "--documentNode";		
+	    } else if(program == FORMATCONVERTER_CLASSIFIERNODE) {
+		command_args[command_args.length-1] = "--classifierNode";		
+	    } 
+	    break;
+	default:
+	    System.err.println("*** Unrecognised program code: " + program);
+	    return outputstr;
+	}
+
+	// Generate the formatconverter command		
+	/*if (Gatherer.isGsdlRemote) {
+	  
+	  }*/
+
+	SafeProcess prcs = new SafeProcess(command_args);
+	prcs.setInputString(inputstr);
+	prcs.setSplitStdErrorNewLines(true);
+	prcs.runProcess();
+	// process done, now get process output
+	outputstr = prcs.getStdOutput();
+	String errmsg = prcs.getStdError();
+	if(!errmsg.equals("")) {
+	    System.err.println("*** Process errorstream: \n" + errmsg + "\n****");
+	}
+
+	///System.err.println("#### Got output: " + outputstr);
+
+	/*
+	try {	    
+	    
+	    Runtime rt = Runtime.getRuntime();
+	    Process prcs = null;
+	    
+	    prcs = rt.exec(command_args);
+
+	    // send inputstr to process
+	    OutputStreamGobbler inputGobbler = new OutputStreamGobbler(prcs.getOutputStream(), inputstr);
+	    
+	    // monitor for any error messages
+            InputStreamGobbler errorGobbler = new InputStreamGobbler(prcs.getErrorStream(), true);
+            
+            // monitor for the expected output line(s)
+            InputStreamGobbler outputGobbler = new InputStreamGobbler(prcs.getInputStream());
+
+            // kick them off
+            inputGobbler.start();
+            errorGobbler.start();
+            outputGobbler.start();
+                                    
+            // any error???
+            process_exitValue = prcs.waitFor();
+            //System.out.println("ExitValue: " + exitVal); 
+
+	    // From the comments of 
+	    // http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
+	    // To avoid running into nondeterministic failures to get the process output
+	    // if there's no waiting for the threads, call join() on each Thread (StreamGobbler) object:
+	    outputGobbler.join();
+	    errorGobbler.join();
+	    inputGobbler.join();
+	    
+	    outputstr = outputGobbler.getOutput();
+	    String errmsg = errorGobbler.getOutput();
+	    if(!errmsg.equals("")) {
+		System.err.println("*** Process errorstream: \n" + errmsg + "\n****");
+	    }
+       
+	} catch(IOException ioe) {
+	    System.err.println("IOexception " + ioe.getMessage());
+	    //ioe.printStackTrace();
+	} catch(InterruptedException ie) {
+	    System.err.println("Process InterruptedException " + ie.getMessage());
+	    //ie.printStackTrace();
+	}
+	*/
+	return outputstr;
+    }
+
+    public void convertGS2FormatStatements() {
+	    		
+	// at this point, we know there are one or more <gsf:format-gs2 /> elements
+	// process each of them as follows: unescape, then call formatconverter, then call html tidy on it
+	//NodeList gsf_format_gs2_list = root.getElementsByTagNameNS("gsf", "format-gs2");
+
+	int len = gsf_format_gs2_list.getLength();
+
+	for(int i = 0; i < len; i++) {
+	        
+	    Element gs2format = (Element)gsf_format_gs2_list.item(i);
+	    String gs2formatstr = XMLTools.getElementTextValue(gs2format); // seems to already unescape the html entities
+	    //gs2formatstr = Codec.transform(gs2formatstr, Codec.ESCAPEDHTML_TO_UNESCAPED);
+	    
+	    processFormatStatement(i, gs2formatstr);
+	} 
+
+	increment(); // modifies the textareas to initialise them
+	setVisible(true);
+    }
+
+
+    private String processFormatStatement(int i, String gs2formatstr) {
+	String errorMsg = "";
+
+	boolean startsWithTableCell = (gs2formatstr.toLowerCase().startsWith("<td")) ? true : false;
+
+	//System.err.println("*** Found: " + gs2formatstr);
+
+	// Running formatconverter. Decide on whether to pass in option --documentNode|--classifierNode
+	int formatConverterProgramMode = formatConverterMode(i);
+	String gs3formatstr = runInteractiveProgram(formatConverterProgramMode, gs2formatstr);
+	gs3formatstr = gs3formatstr.replaceAll(">\\s+<", "><");
+
+	//System.err.println("*** Format is now: " + gs3formatstr);
+
+	String gs3formatstr_notags = gs3formatstr;
+	gs3formatstr = addSurroundingTags(gs3formatstr);
+
+
+	String validationMsg = XMLTools.parseDOM(gs3formatstr);
+	if(!validationMsg.startsWith(XMLTools.WELLFORMED)) {
+	    // Run Html Tidy in XML mode
+	    //System.err.println("*** Needing to run HTML Tidy on: ");
+	    //System.err.println(gs3formatstr_notags);	    
+
+
+	    // HTMLTidy returns 0 if no warnings or errors, 1 if just Warnings, 2 if Errors (failure)
+	    // http://sourceforge.net/p/tidy/mailman/tidy-develop/thread/20020811204058.GD1137@shaw.ca/
+	    String htmltidy_string = runInteractiveProgram(XMLTIDY, gs3formatstr_notags);
+
+	    if(process_exitValue >= 2) {
+		// System.err.println("@@@ Process exit value: " + process_exitValue);
+		errorMsg = Dictionary.get("FormatConversionDialog.Tidy_Failed") 
+		    + " " + Dictionary.get("FormatConversionDialog.XML_Still_Invalid");
+	    } else {
+		errorMsg = "";
+
+		gs3formatstr_notags = htmltidy_string;
+
+		gs3formatstr_notags = removeHTMLTags(i, gs3formatstr_notags, startsWithTableCell);
+		gs3formatstr = addSurroundingTags(gs3formatstr_notags); 
+		
+		// Having applied html-tidy, setGS3Format() will return true if it finally parsed
+	    }
+	}
+	
+	// For now, assume HTML Tidy has worked and that the gs3format has now been parsed successfully
+
+	boolean parsed = setGS3Format(i, gs3formatstr); // will parse the gs3formatstr into a DOM object
+	if(!parsed && errorMsg.equals("")) {
+	    errorMsg = Dictionary.get("FormatConversionDialog.Tidy_Done")
+		+ " " + Dictionary.get("FormatConversionDialog.XML_Still_Invalid");
+	}	
+	return errorMsg;	
+	//return gs3formatstr;
+    }
+
+    // HTML tidy adds entire HTML tags around a single format statement. This method removes it.
+    private String removeHTMLTags(int i, String gs3formatstr_notags, boolean startsWithTD) {
+
+	// if it's a VList classifier <gsf:template match="documentNode|classifierNode"/>
+	// and the gs2 format statement starts with a <td>, 
+	// then remove up to and including the outermost <tr>, 
+	// else remove up to and including the <body> tag
+	
+	String removeOuterTag = "body>";
+	Element parent = (Element)getParentNode(i); // gets parent of GS2format: <gsf:template>
+	if(parent.hasAttribute("match") && (!parent.hasAttribute("mode") || !parent.getAttribute("mode").equals("horizontal"))) {
+	    if(startsWithTD) {
+		removeOuterTag = "tr>"; // remove the outermost <tr> cell HTML tidy added around the tablecell
+	    }
+	}
+
+	// <unwanted>
+	// <removeThisOuterTag>
+	// lines we want
+	// </removeThisOuterTag>
+	// <unwanted>
+	
+	int end = gs3formatstr_notags.indexOf(removeOuterTag);
+	if(end != -1) {
+	    gs3formatstr_notags = gs3formatstr_notags.substring(end+removeOuterTag.length());
+	}
+	int start = gs3formatstr_notags.lastIndexOf("</"+removeOuterTag); //closing tag
+	if(start != -1) {
+	    gs3formatstr_notags = gs3formatstr_notags.substring(0, start);
+	}
+
+	//System.err.println("@@@@ " + startsWithTD + " - TAG: " + removeOuterTag + " - AFTER REMOVING TAGS:\n" + gs3formatstr_notags);
+
+	return gs3formatstr_notags;	
+    }
+
+
+    //**************** ACCESS FUNCTIONS ***************//
+
+    // gs2 format text is the text string that goes into <gsf:format-gs2/>: <gsf:format-gs2>text</gsf:format-gs2>
+    private void setGS2Format(int i, String text) {
+	XMLTools.setElementTextValue(getGS2Format(i), text);
+    }
+    
+    // gs3FormatStr represents DOM, and must be parsed and appended as sibling to the gs2format element
+    // as <gsf:gs3-root/>. If it fails to parse, nest it in a CDATA element of <gsf:gs3-root/>
+    private boolean setGS3Format(int i, String gs3formatstr) {
+
+	Document ownerDoc = getGS2Format(i).getOwnerDocument();
+	Node gs3format = null;
+	boolean parse_success = false;
+
+	
+	String validationMsg = XMLTools.parseDOM(gs3formatstr);
+	if(!validationMsg.startsWith(XMLTools.WELLFORMED)) {	    
+	    // silently add the gs3formatstr in a CDATA block into the root
+
+	    gs3formatstr = removeSurroundingTags(gs3formatstr);
+	    gs3format = ownerDoc.createElement(GSF_GS3_ROOT_TAG);	    
+	    Node cdataSection = ownerDoc.createCDATASection(gs3formatstr);
+	    gs3format.appendChild(cdataSection);
+	    parse_success = false;
+	}
+	else {
+
+	    // parse DOM into XML
+	    Document doc = XMLTools.getDOM(gs3formatstr);
+	    Element root = doc.getDocumentElement();
+	    gs3format = ownerDoc.importNode(root, true); // an element
+	    parse_success = true;
+
+	    //	System.err.println("@@@@ ROOT:\n" + XMLTools.xmlNodeToString(root));
+	}
+
+	// add gs3format element as sibling to gs2format element
+	Element oldgs3format = getGS3Format(i);
+	if(oldgs3format == null) {	    
+	    getParentNode(i).appendChild(gs3format);
+	    //	    System.err.println("@@@ APPEND");
+	} else {
+	    // replace the existing
+	    getParentNode(i).replaceChild(gs3format, oldgs3format);
+	    //	    System.err.println("@@@ REPLACE");
+	}
+
+	//http://stackoverflow.com/questions/12524727/remove-empty-nodes-from-a-xml-recursively
+
+	return parse_success;
+    }
+
+    private Node getParentNode(int i) {
+	return gsf_format_gs2_list.item(i).getParentNode();
+    }
+
+    private Element getGS2Format(int i) {
+	return (Element)gsf_format_gs2_list.item(i);
+    }
+
+    private String getGS2FormatString(int i) {
+	return XMLTools.getElementTextValue(getGS2Format(i));
+    }
+
+    private Element getGS3Format(int i) {
+	Element parent  = (Element)getParentNode(i);
+	NodeList nl = parent.getElementsByTagName(GSF_GS3_ROOT_TAG);
+	if(nl.getLength() > 0) {
+	    return (Element)nl.item(0);
+	}
+	return null;
+    }
+
+    private String getGS3FormatString(int i) {	
+	Element gs3format = getGS3Format(i);
+	if(gs3format == null) {
+	    return "";
+	} else {
+	    
+	    // if any child is a CData section, return its text as-is. There will be no indenting
+	    NodeList children = gs3format.getChildNodes();
+	    for(int j = 0 ; j < children.getLength(); j++) {
+		if(children.item(j).getNodeType() == Node.CDATA_SECTION_NODE) {		    
+		    return children.item(j).getNodeValue(); // content of CDataSection
+		}
+	    }
+	    
+	    // else we have proper nodes, return indented string
+	    StringBuffer sb = new StringBuffer();
+	    XMLTools.xmlNodeToString(sb, gs3format, true, "  ", 0);
+	    return sb.toString();
+	}
+    }
+
+    private int formatConverterMode(int i) {
+	String docOrClassNodeType = "";
+
+	// Given XML of the form:
+	// <browse|search>
+	//   <format>
+	//     <gsf:template match="documentNode|classifierNode" [mode=horizontal]>
+	//       <gsf-format:gs2 />
+	//       <gs3format/>
+	//   </format>
+	// </browse|search>
+
+	Element parent = (Element)getParentNode(i); // gets parent of GS2format: <gsf:template>
+	String nodeType = parent.getAttribute("match"); //e.g. documentNode, classifierNode, or "" if no match attr
+	
+	if(nodeType.equals("documentNode")) {
+	    return FORMATCONVERTER_DOCUMENTNODE; // convert just the part of the format stmt applicable to documentNodes
+	} else if(nodeType.equals("classifierNode")) {
+	    return FORMATCONVERTER_CLASSIFIERNODE; // convert just the part of the format stmt applicable to classifierNodes
+	}
+	return FORMATCONVERTER;	// convert the entire format statement
+    }
+
+    private String getLabel(int i) {
+	String label = "";
+
+	// Given XML of the form:
+	// <browse|search>
+	//   <format>
+	//     <gsf:template match="documentNode|classifierNode" [mode=horizontal]>
+	//       <gsf-format:gs2 />
+	//       <gs3format/>
+	//   </format>
+	// </browse|search>
+
+	// Want to return the label: "browse|search > documentNode|classifierNode [> horizontal]"
+
+	Element parent = (Element)getParentNode(i); // gets parent of GS2format: <gsf:template>
+	label = parent.getAttribute("match"); //e.g. documentNode, classifierNode
+
+	if(parent.hasAttribute("mode")) { // e.g. classifierNode mode=horizontal
+	    label = label + " > " + parent.getAttribute("mode");
+	}
+
+	Element ancestor = (Element) parent.getParentNode().getParentNode(); // ancestor: <browse>|<search>
+	label = ancestor.getTagName() + " > " + label;
+
+	return label;
+    }
+
+    // gs2 format statements have spaces instead of newlines. For display, replace with newlines
+    private String makeLines(String gs2formatstr) {
+	return gs2formatstr.replaceAll(">\\s+<", ">\n<");
+    }
+   
+    private String singleLine(String gs2formatstr) {
+	return gs2formatstr.replace(">\n<", "> <"); // put the spaces back
+    }
+
+    private String removeSurroundingTags(String xml)
+    {
+	return xml.replaceAll("<"+GSF_GS3_ROOT_TAG+" xmlns:gsf=(\"|\')http://www.greenstone.org/greenstone3/schema/ConfigFormat(\"|\') xmlns:gslib=(\"|\')http://www.greenstone.org/skinning(\"|\') xmlns:xsl=(\"|\')http://www.w3.org/1999/XSL/Transform(\"|\')>\n?", "").replaceAll("\n?</"+GSF_GS3_ROOT_TAG+">", "");//.trim();
+    }
+
+    private String addSurroundingTags(String gs3formatstr) {
+	return "<"+GSF_GS3_ROOT_TAG+" xmlns:gsf='http://www.greenstone.org/greenstone3/schema/ConfigFormat' xmlns:gslib='http://www.greenstone.org/skinning' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>"+gs3formatstr+"</"+GSF_GS3_ROOT_TAG+">";
+    }
+
+    //*************************FUNCTIONS THAT INTERACT WITH WIDGETS************************//
+    // increment() loads the next values into the dialog
+    private boolean increment() {
+	current_index++;
+	section_label.setText ( getLabel(current_index) );
+	gs2_textarea.setText( makeLines(getGS2FormatString(current_index)) );
+	gs3_textarea.setText( removeSurroundingTags(getGS3FormatString(current_index)) );
+	statusbar.setText("");
+
+	// as we're on a new screen of dialog, need to clear all undo/redo history
+	gs2_textarea.discardAllEdits(); // will disable redo/undo buttons
+	gs3_textarea.discardAllEdits();
+
+	int len = gsf_format_gs2_list.getLength();
+	count_label.setText((current_index+1) + " / " + len);
+	if((current_index+1) == len) {
+	    return false;
+	} else {
+	    return true;
+	}	
+    }
+
+
+    private void setStatus(String msg) {
+	statusbar.setText(msg);
+    }
+    private void setErrorStatus(String msg) {
+	statusbar.setBackground(Color.red);
+	statusbar.setText(msg);
+    }
+
+
+    public void dispose() {
+	//System.err.println("@@@@ DIALOG CLOSING!");
+	if(dlgResult != OpenCollectionDialog.CANCEL_OPTION) {
+	    // Need to remove the <gsf:gs3-root/> siblings of all <gsf:format-gs2/>
+	    // Then, get the children of each <gsf:gs3-root/> and add these as siblings of <gsf:format-gs2/>
+	    
+	    
+	    int len = gsf_format_gs2_list.getLength();
+	    for(int k=len-1; k >= 0; k--) {
+		Element parent = (Element)getParentNode(k);		
+		
+		//parent.normalize();
+
+		NodeList children = parent.getChildNodes();
+
+		// now have to loop/remove nodes in reverse order, since the following loop 
+		// modifies the very nodelist we're looping over by removing nodes from it
+
+		int numChildren = children.getLength()-1;
+
+		for(int i=numChildren; i >= 0; i--) {
+		//for(int i = 0; i < children.getLength(); i++) {
+		    Node child = children.item(i);
+
+		    if(child.getNodeName().equals(GSF_FORMAT_GS2_TAG)) { 
+			// if we're dealing with gs2-format-stmts, put their textnode contents in CData sections
+			// http://www.w3schools.com/xml/xml_cdata.asp
+			// This avoids having to look at html-encoded gs2-format tags in the Format pane
+
+			Element gs2format = (Element)child;
+			String gs2formatstr = XMLTools.getElementTextValue(gs2format);
+			gs2formatstr = Codec.transform(gs2formatstr, Codec.ESCAPEDHTML_TO_UNESCAPED);
+			
+			Node textNode = XMLTools.getNodeTextNode(gs2format);
+			Node cdataSection = gs2format.getOwnerDocument().createCDATASection(gs2formatstr); 
+			gs2format.replaceChild(cdataSection, textNode);
+		    } 
+		    else if(child.getNodeName().equals(GSF_GS3_ROOT_TAG)) { 
+
+			// remove GS3 node and append its children to the parent in its place
+			// the <gsf:gs3-root /> elements wouldn't be in the xml_file_doc DOM tree
+			// unless they were valid XML, so don't need to check for validity here
+			
+			Node gs3root = child;
+			NodeList gs3_format_lines = gs3root.getChildNodes();
+
+			for(int j = 0; j < gs3_format_lines.getLength(); j++) {
+			    Node duplicate = gs3_format_lines.item(j).cloneNode(true);
+			    parent.appendChild(duplicate);			    
+			}
+			gs3root = parent.removeChild(gs3root);
+			gs3root = null; // finished processing
+
+		    } // else - skip all nodes other than <gsf:format-gs2/> and <gsf:gs3-root/>
+		}
+	    }
+ 
+	    Element root = xml_file_doc.getDocumentElement();
+	    //System.err.println("### XML file to write out:\n" + XMLTools.xmlNodeToString(root));
+	    
+	    // Finally, write out the collection xml file
+	    String[] nonEscapingTagNames = { StaticStrings.FORMAT_STR, StaticStrings.DISPLAYITEM_STR };
+	    XMLTools.writeXMLFile(collect_cfg_file, xml_file_doc, nonEscapingTagNames);
+	    
+	}
+	super.dispose();
+	
+    }
+
+    //******************INNER CLASSES including LISTENERS and STREAMGOBBLERS****************//
+
+    // windowClosing() is called when the user presses the top-right close button the dialog
+    // this means the user wanted to cancel out of the entire Format Conversion Wizard.
+    private class WindowClosingListener extends WindowAdapter {
+	public void windowClosing(WindowEvent e) {	    
+	    dlgResult = OpenCollectionDialog.CANCEL_OPTION;
+	}
+    }
+
+    private class ReconvertListener implements ActionListener {
+	public void actionPerformed(ActionEvent e) {
+	    String gs2formatstr = singleLine(gs2_textarea.getText());
+	    String errorMsg = processFormatStatement(current_index, gs2formatstr);
+	    gs3_textarea.setText( removeSurroundingTags(getGS3FormatString(current_index)) );
+	    if(!errorMsg.equals("")) {
+		setErrorStatus(errorMsg);
+	    } else {
+		statusbar.setText("");
+	    }
+	}
+    }
+
+    
+    private class NextButtonListener implements ActionListener {
+	public void actionPerformed(ActionEvent e) {
+	    //statusbar.setText("");
+
+	    // check if the GS3 format statement is valid XML before storing. If not, let user to decide 
+	    // whether they want to re-edit it or if they want to keep it as-is, in which case it needs
+	    // to be stored as CDATA, which will make it an inactive format statement. 
+	    // See http://www.w3schools.com/xml/xml_cdata.asp
+	    // setGS3Format() already stores invalidXML as CDATA.
+	    
+	    // Check if the GS3 format statement is valid XML before storing. If not, let the
+	    // user to decide whether they want to re-edit it or store it as-is and continue	    
+
+	    // user okay-ed the lines currently displayed, store them
+	    setGS2Format( current_index, singleLine(gs2_textarea.getText()) );
+	    boolean parse_success = setGS3Format( current_index, addSurroundingTags(gs3_textarea.getText()) );
+
+	    if(!parse_success) { // invalid XML, warn the user
+		String message = Dictionary.get("FormatConversionDialog.Invalid_XML") + " " + Dictionary.get("FormatConversionDialog.Cancel_Or_Continue_Next");
+		int user_choice = JOptionPane.showConfirmDialog(FormatConversionDialog.this, message, WARNING_TITLE, JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
+
+		if(user_choice == JOptionPane.CANCEL_OPTION) {
+		    return; // do nothing on this NextButton press. Don't increment. Let user re-adjust invalid XML for GS3 statement.
+		}
+	    }
+	    
+	    if(increment()) {
+		repaint();
+	    } else {
+		next_button.setEnabled(false);
+		getRootPane().setDefaultButton(accept_all_button); 
+
+	    }
+	    
+	}
+    }
+
+    private class CancelButtonListener implements ActionListener {
+	public void actionPerformed(ActionEvent e) {
+	     dlgResult = OpenCollectionDialog.CANCEL_OPTION;
+	     FormatConversionDialog.this.dispose(); // close dialog
+	}
+    }
+
+
+    private class AcceptAllButtonListener implements ActionListener {
+	public void actionPerformed(ActionEvent e) {
+
+	    // user okay-ed the lines, store them
+	    setGS2Format(current_index, gs2_textarea.getText());
+	    String gs3formatstr = gs3_textarea.getText();
+	    boolean parse_success = setGS3Format(current_index, addSurroundingTags(gs3formatstr));
+	    String message = "";
+
+	    if(!parse_success) { // invalid XML for current format statement, warn the user
+		setErrorStatus(Dictionary.get("FormatConversionDialog.Invalid_XML"));
+
+		message = Dictionary.get("FormatConversionDialog.Invalid_XML") + " " + Dictionary.get("FormatConversionDialog.Cancel_Or_Continue_Next");
+	    }
+
+	    // Even if the current GS3 format statement is valid XML, the user could have pressed 
+	    // Accept All at the very start of the FormatConversion dialog too. Check all the 
+	    // subsequent format statements, and if any have invalid XML, warn user.
+	    for(int i = current_index+1; parse_success && i < gsf_format_gs2_list.getLength(); i++) {		
+		gs3formatstr = getGS3FormatString(i);
+		String validationMsg = XMLTools.parseDOM(gs3formatstr);
+		if(!validationMsg.startsWith(XMLTools.WELLFORMED)) {
+		    parse_success = false;
+		    message = Dictionary.get("FormatConversionDialog.Cancel_Or_Accept_All");
+		}
+	    }
+
+	    if(!parse_success) {
+		int user_choice = JOptionPane.showConfirmDialog(FormatConversionDialog.this, message, WARNING_TITLE, JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
+		if(user_choice == JOptionPane.CANCEL_OPTION) {
+		    return; // Don't close the dialog. Let the user continue looking at this or subsequent GS3 format statements.
+		}
+	    }
+
+	    // If we're here, then either the format statements parsed, or the user accepted them anyway
+	    FormatConversionDialog.this.dispose(); // close dialog
+	}
+    }
+
+    private class XMLTidyButtonListener implements ActionListener {
+	// run HTML tidy taking input from stdin
+	// http://www.w3.org/People/Raggett/tidy/
+	public void actionPerformed(ActionEvent e) {
+	    String gs3formatstr_notags = gs3_textarea.getText();
+
+	    // Flag to determine which tags that HTML tidy adds need to be removed
+	    boolean startsWithTableCell = (gs3formatstr_notags.trim().toLowerCase().startsWith("<td")) ? true : false;
+	    // run HTML tidy on the GS3 format statement
+	    String htmltidy_string = runInteractiveProgram(XMLTIDY, gs3formatstr_notags);
+
+	    if(process_exitValue >= 2) {
+		//System.err.println("@@@ Process exit value: " + process_exitValue);
+		setErrorStatus(Dictionary.get("FormatConversionDialog.Tidy_Failed"));
+	    } else {
+		gs3formatstr_notags = htmltidy_string;
+		
+		// HTML tidy adds extra HTML markup around the formatstring, so will need to remove it:
+		gs3formatstr_notags = removeHTMLTags(current_index, gs3formatstr_notags, startsWithTableCell);
+		
+		// put the XML tags around the gs3 format string again so we can convert it to a DOM object
+		String gs3formatstr = addSurroundingTags(gs3formatstr_notags);
+		
+		boolean parse_success = setGS3Format(current_index, gs3formatstr); // converts to DOM object
+		
+		// Get indented GS3 format string from DOM object and display it in the text area
+		gs3_textarea.setText( removeSurroundingTags(getGS3FormatString(current_index)) );
+
+		if(parse_success) {
+		    statusbar.setText(Dictionary.get("FormatConversionDialog.Tidy_Done"));
+		} else {
+		    setErrorStatus(Dictionary.get("FormatConversionDialog.Tidy_Done") 
+				   + " " + Dictionary.get("FormatConversionDialog.Error_GS3_Format") 
+				   + " " + Dictionary.get("FormatConversionDialog.Invalid_XML"));
+		}
+	    }
+	}
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FormatPane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FormatPane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/FormatPane.java	(revision 31635)
@@ -0,0 +1,247 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.cdm.FormatManager;
+import org.greenstone.gatherer.cdm.Format;
+import org.greenstone.gatherer.cdm.Format4gs3Manager;
+import org.greenstone.gatherer.cdm.Format4gs3;
+import org.greenstone.gatherer.cdm.DOMProxyListEntry;
+import org.greenstone.gatherer.cdm.Control;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.shell.GShellEvent;
+import org.greenstone.gatherer.shell.GShellListener;
+
+public class FormatPane
+extends BaseConfigPane
+implements PreviewButtonOwner,GShellListener{
+    
+    
+    /** The button for viewing the collection. */
+    private static PreviewButton preview_button = null;
+    private boolean buildCanceled = false;
+    
+    /** The constructor. */
+    public FormatPane () {
+        super ();
+        if (Gatherer.GS3) {
+            contents = new String []{ "CDM.GUI.General", "CDM.GUI.SearchMetadata", "CDM.GUI.Formats", "CDM.GUI.Translation" };
+        } else {
+            contents = new String []{ "CDM.GUI.General", "CDM.GUI.SearchMetadata", "CDM.GUI.Formats", "CDM.GUI.Translation", "CDM.GUI.SuperCollection","CDM.GUI.Macros", "CDM.GUI.DepositorMetadata" };
+        }
+        JPanel side_panel = new JPanel ();
+        side_panel.setComponentOrientation(Dictionary.getOrientation());
+        side_panel.setLayout (new BorderLayout ());
+        remove (tree_pane);
+        side_panel.add (tree_pane, BorderLayout.CENTER);
+        JPanel preview_pane = new JPanel ();
+        preview_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+        preview_pane.setLayout (new BorderLayout ());
+        preview_pane.setBorder (BorderFactory.createEmptyBorder (5,0,0,0));
+        
+        preview_button = new PreviewButton (Dictionary.get ("CreatePane.Preview_Collection"), Dictionary.get ("CreatePane.Preview_Collection_Tooltip"));
+        preview_button.setEnabled (true);
+        preview_button.setVariablePreview (true);
+        preview_button.setOwner (this);
+        preview_pane.add (preview_button, BorderLayout.CENTER);
+        
+        
+        preview_button.addActionListener (new ActionListener () {
+            public void actionPerformed (ActionEvent event) {
+                if (view != null) {
+                    view.loseFocus ();
+                }
+                
+                if (buildCanceled){
+                    WarningDialog dialog = new WarningDialog ("warning.ShowPreviousCollection", Dictionary.get ("ShowPreviousCollection.Title"), Dictionary.get ("ShowPreviousCollection.Message"), null, false);
+                    dialog.display ();
+                    dialog.dispose ();
+                }
+            }
+            
+        });
+        side_panel.add (preview_pane, BorderLayout.SOUTH);
+        
+        add (side_panel, BorderLayout.LINE_START);
+    }
+    
+    public void gainFocus (){
+        
+        if (Gatherer.c_man.built () && Configuration.library_url != null) {
+            preview_button.setEnabled (true);
+        }
+        else{
+            preview_button.setEnabled (false);
+        }
+        
+    }
+    public static void setPreviewButton (boolean ready_to_preview) {
+	// if we're asked to enable the preview button, yet the collection is still
+	// unbuilt (as could happen when someone clicks on a format statement before 
+	// building the collection), then shouldn't enable previewing.
+	if(ready_to_preview && !Gatherer.c_man.built()) {
+	    ready_to_preview = false;
+	}
+        preview_button.setEnabled (ready_to_preview);
+    }
+    
+    /** All implementation of GShellListener must include this method so the listener can be informed of messages from the GShell.
+     * @param event A <strong>GShellEvent</strong> that contains, amoung other things, the message.
+     */
+    public synchronized void message (GShellEvent event) {
+        // We don't care.
+    }
+    
+    
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell begins its task. Implementation side-effect, not actually used.
+     * @param event A <strong>GShellEvent</strong> that contains details of the initial state of the <strong>GShell</strong> before task comencement.
+     */
+    public synchronized void processBegun (GShellEvent event) {
+        buildCanceled = false;
+    }
+    
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell completes its task.
+     * @param event A <strong>GShellEvent</strong> that contains details of the final state of the <strong>GShell</strong> after task completion.
+     */
+    public synchronized void processComplete (GShellEvent event) {
+        if(event.getStatus () == GShell.OK) {
+            if(event.getType () == GShell.BUILD) {
+                buildCanceled = false;
+            }
+        }
+        else {
+            buildCanceled = true;
+        }
+    }
+    
+    
+    
+    protected Control getSubControls (String type) {
+        if(type.equals ("CDM.GUI.General")) {
+            return CollectionDesignManager.general_manager.getControls ();
+        }
+        if (type.equals ("CDM.GUI.SearchMetadata")) {
+            return CollectionDesignManager.searchmetadata_manager.getControls ();
+        }
+        if(type.equals ("CDM.GUI.Formats")) {
+            return CollectionDesignManager.format_manager.getControls ();
+        }
+        if (type.equals ("CDM.GUI.Translation")) {
+            return CollectionDesignManager.translation_manager.getControls ();
+        }
+        if (type.equals ("CDM.GUI.Macros")) {
+            return CollectionDesignManager.macros_manager.getControls ();
+        }
+        if(type.equals ("CDM.GUI.SuperCollection")) {
+            return CollectionDesignManager.supercollection_manager.getControls ();
+        }
+	if(type.equals ("CDM.GUI.DepositorMetadata")) {
+	    return CollectionDesignManager.depositormetadata_manager.getControls ();
+	}
+        return null;
+    }
+    
+    private int page_type = PreviewButton.HOME_PAGE;
+    private String page_params = "";
+    public int getPageType () {
+        System.err.println ("view type = "+view_type);
+        if (view_type.equals ("CDM.GUI.General") || view_type.equals ("CDM.GUI.Translation")) {
+            page_type = PreviewButton.HOME_PAGE;
+        } else {
+            
+            if (Gatherer.GS3 == true) {
+                Format4gs3 current_format = ((Format4gs3Manager.FormatControl)view).getCurrentFormat ();
+                String feature_name = current_format.getFeatureName ();
+                
+                if (feature_name.equals ("browse")) {
+                    page_type = PreviewButton.CLASSIFIER_PAGE;
+                    page_params = "CL1";
+                } else
+                if (feature_name.startsWith ("CL")) {
+                    page_type = PreviewButton.CLASSIFIER_PAGE;
+                    page_params = current_format.getFeatureName ();
+                }
+                else if (feature_name.startsWith ("display")) {
+                    page_type = PreviewButton.DOCUMENT_PAGE;
+                    page_params = "HASH01e4da6100fdbb11b7ef244b";
+                }
+                else if (feature_name.equals ("search")) {
+                    page_type = PreviewButton.SEARCH_PAGE;
+                    page_params = "the";
+                } else { // HOME_PAGE 
+                }
+            } else {
+                Format current_format = ((FormatManager.FormatControl)view).getCurrentFormat ();
+                String feature_name = current_format.getFeatureName ();
+                System.err.println ("current format = "+feature_name);
+                if (feature_name.equals ("")) {
+                    // must be modifying VList or HList
+                    // what should we do???
+                    page_type = PreviewButton.CLASSIFIER_PAGE;
+                    page_params = "CL1";
+                }
+                if (feature_name.startsWith ("CL")) {
+                    page_type = PreviewButton.CLASSIFIER_PAGE;
+                    page_params = current_format.getFeatureName ();
+                }
+                else if (feature_name.startsWith ("Document")) {
+                    page_type = PreviewButton.DOCUMENT_PAGE;
+                    page_params = "HASH01e4da6100fdbb11b7ef244b";
+                }
+                else if (feature_name.equals ("Search")) {
+                    page_type = PreviewButton.SEARCH_PAGE;
+                    page_params = "the";
+                }
+            }
+        }
+        return page_type;
+    }
+    
+    public String getPageParams () {
+        return page_params;
+    }
+    
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GComboBox.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GComboBox.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GComboBox.java	(revision 31635)
@@ -0,0 +1,398 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.plaf.basic.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.util.Utility;
+
+/**
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class GComboBox
+    extends JComboBox {
+
+    private Color background = null;
+    private Color foreground = null;
+    private Color editable_background = null;
+    private Color editable_foreground = null;
+    private Color selection_background = null;
+    private Color selection_foreground = null;
+
+    private boolean sort_objects = true;
+
+    public GComboBox() {
+	super();
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	init();
+	setOpaque(!Utility.isMac());
+    }
+
+    public GComboBox(boolean editable) {
+	super();
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	setEditable(editable);
+	init();
+
+    }
+
+    public GComboBox(ArrayList data) {
+	super(data.toArray());
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	init();
+    }
+
+    public GComboBox(ArrayList data, boolean editable) {
+	super(data.toArray());
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	setEditable(editable);
+	init();
+    }
+
+    public GComboBox(ArrayList data, boolean editable, boolean sorted) {
+	super(data.toArray());
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	setEditable(editable);
+	setSorted(sorted);
+	init();
+    }
+
+    public GComboBox(ComboBoxModel model) {
+	super(model);
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	init();
+    }
+
+    public GComboBox(ComboBoxModel model, boolean editable) {
+	super(model);
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	setEditable(editable);
+	init();
+    }
+
+    public GComboBox(Object data[]) {
+	super(data);
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	init();
+    }
+
+    public GComboBox(Object data[], boolean editable) {
+	super(data);
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	setEditable(editable);
+	init();
+    }
+
+    public GComboBox(Object data[], boolean editable, boolean sorted) {
+	super(data);
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	setEditable(editable);
+	setSorted(sorted);
+	init();
+    }
+
+    public GComboBox(Vector data) {
+	super(data);
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	init();
+    }
+
+    public GComboBox(Vector data, boolean editable) {
+	super(data);
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	setOpaque(!Utility.isMac());
+	setEditable(editable);
+	init();
+    }
+
+    public void setSorted(boolean sort) {
+	sort_objects = sort;
+    }
+    public int add(Object object) {
+	if (dataModel instanceof Model) {
+	    return ((Model) dataModel).add(object);
+	}
+	else {
+	    return -1;
+	}
+    }
+
+    public Object get(int index) {
+	return dataModel.getElementAt(index);
+    }
+
+    public void clear() {
+	if (dataModel instanceof Model) {
+	    ((Model) dataModel).clear();
+	}
+    }
+
+    public void init() {
+	Model model = new Model();
+	ComboBoxModel old_model = (ComboBoxModel) getModel();
+	setModel(model);
+	setOpaque(true);
+
+	// Restore any data given into our model
+	for(int i = 0; i < old_model.getSize(); i++) {
+	    model.add(old_model.getElementAt(i));
+	}
+
+	// Change component
+	UI ui = new UI();
+	setUI(ui);
+	ui.setButtonBackground();
+
+	// Initialization
+	this.background = Configuration.getColor("coloring.collection_tree_background", false);
+	this.foreground = Configuration.getColor("coloring.collection_tree_foreground", false);
+	this.editable_background = Configuration.getColor("coloring.editable_background", false);
+	this.editable_foreground = Configuration.getColor("coloring.editable_foreground", false);
+	this.selection_background = Configuration.getColor("coloring.collection_selection_background", false);
+	this.selection_foreground = Configuration.getColor("coloring.collection_selection_foreground", false);
+	if (isEditable()) {
+	    this.setBackground(editable_background);
+	    this.setForeground(editable_foreground);
+	}
+	else {
+	    this.setBackground(background);
+	    this.setForeground(foreground);
+	}
+	setBorder(BorderFactory.createLoweredBevelBorder());
+	setRenderer(new Renderer());
+    }
+
+    /** Overridden to do nothing. 
+     * @param background
+     */
+    public void setBackground(Color background) {
+    }
+
+    public void setBackgroundEditableColor(Color editable_background) {
+	this.editable_background = editable_background;
+    }
+
+    public void setBackgroundNonSelectionColor(Color background) {
+	this.background = background;
+    }
+
+    public void setBackgroundSelectionColor(Color selection_background) {
+	this.selection_background = selection_background;
+    }
+
+    public void setEditable(boolean editable) {
+	setEditor(new Editor());
+	super.setEditable(editable);
+	if (isEditable()) {
+	    this.setBackground(editable_background);
+	    this.setForeground(editable_foreground);
+	}
+	else {
+	    this.setBackground(background);
+	    this.setForeground(foreground);
+	}
+    }
+
+    public void setTextEditableColor(Color editable_foreground) {
+	this.editable_foreground = editable_foreground;
+    }
+
+    public void setTextNonSelectionColor(Color foreground) {
+	this.foreground = foreground;
+    }
+
+    public void setTextSelectionColor(Color selection_foreground) {
+	this.selection_foreground = selection_foreground;
+    }
+
+
+    private class Editor
+	extends JTextField
+	implements ComboBoxEditor {
+	public Editor() {
+	    setOpaque(true);
+	    if (isEditable()) {
+		setBackground(editable_background);
+		setForeground(editable_foreground);
+	    }
+	    else {
+		setBackground(background);
+		setForeground(foreground);
+	    }
+	    setSelectionColor(selection_background);
+	    setSelectedTextColor(selection_foreground);
+	}
+
+	public Component getEditorComponent() {
+	    return this;
+	}
+
+	public Object getItem() {
+	    return getText();
+	}
+
+	public void setItem(Object item) {
+	    if(item != null) {
+		setText(item.toString());
+	    }
+	    else {
+		setText("");
+	    }
+	}
+    }
+
+    private class Model
+	extends DefaultComboBoxModel {
+	
+	
+	public int add(Object extension) {
+	    int position = 0;
+	    String extension_str = extension.toString().toLowerCase();
+	    while(extension != null && position < getSize()) {
+		String sibling = getElementAt(position).toString().toLowerCase();
+		int order = extension_str.compareTo(sibling);
+		// If we are now less than the sibling, insert here.
+		if(sort_objects && order < 0) {
+		    insertElementAt(extension, position);
+		    extension = null;
+		}
+		// We are equal to the sibling, thus we already exist in list. Done.
+		else if(order == 0) {
+		    extension = null;
+		}
+		// Continue searching.
+		else {
+		    position++;
+		}
+	    }
+	    if(extension != null) {
+		position = getSize();
+		addElement(extension);
+	    }
+	    return position;
+	}
+
+	public void clear() {
+	    removeAllElements();
+	}
+    }
+
+    private class Renderer
+	extends JLabel
+	implements ListCellRenderer {
+	public Renderer() {
+	    super("");
+	    this.setOpaque(true);
+            this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	}
+	public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+	    if(value != null) {
+		this.setText(value.toString());
+	    }
+	    else {
+		this.setText("");
+	    }
+	    if(isSelected) {
+		this.setBackground(selection_background);
+		this.setForeground(selection_foreground);
+	    }
+	    else if (isEditable()) {
+		this.setBackground(editable_background);
+		this.setForeground(editable_foreground);
+	    }
+	    else {
+		this.setBackground(background);
+		this.setForeground(foreground);
+	    }
+	    return this;
+	}
+    }
+
+    private class UI
+	extends BasicComboBoxUI {
+	public UI() {
+	    super();
+	}
+
+	protected ComboPopup createPopup() {
+	    BasicComboPopup popup = new BasicComboPopup(comboBox);
+	    // ---- I don't know why this code is here... maybe it is needed for the Mac? ----
+// 	    {
+// 		    public void show() {
+// 			if(comboBox.getMaximumRowCount() > 0) {
+// 			    Dimension popupSize = new Dimension( comboBox.getWidth(), getPopupHeightForRowCount( comboBox.getMaximumRowCount()));
+// 			    Rectangle popupBounds = new Rectangle( 0, 0, popupSize.width, popupSize.height);
+// 			    scroller.setMaximumSize( popupBounds.getSize() );
+// 			    scroller.setPreferredSize( popupBounds.getSize() );
+// 			    scroller.setMinimumSize( popupBounds.getSize() );
+// 			    list.invalidate();
+// 			    int selectedIndex = comboBox.getSelectedIndex();
+// 			    if ( selectedIndex == -1 ) {
+// 				list.clearSelection();
+// 			    }
+// 			    else {
+// 				list.setSelectedIndex( selectedIndex );
+// 			    }
+// 			    list.ensureIndexIsVisible( list.getSelectedIndex() );
+// 			    setLightWeightPopupEnabled( comboBox.isLightWeightPopupEnabled() );
+// 			    show(arrowButton, arrowButton.getSize().width - (popupSize.width + 4), arrowButton.getSize().height);
+// 			}
+// 		    }
+// 		};
+	    popup.getAccessibleContext().setAccessibleParent(comboBox);
+	    return popup;
+	}
+
+	public void setButtonBackground() {
+		arrowButton.setBackground(Configuration.getColor("coloring.button_background", false));
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GLIButton.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GLIButton.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GLIButton.java	(revision 31635)
@@ -0,0 +1,85 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import org.greenstone.gatherer.util.Utility;
+
+/** A button that knows whether it should paint itself as opaque or otherwise. */
+public class GLIButton
+    extends JButton {
+
+    public GLIButton() {
+	super();
+	setOpaque(!Utility.isMac());
+    }
+
+    public GLIButton(ImageIcon icon) {
+	super(icon);
+	setOpaque(!Utility.isMac());
+    }
+
+    public GLIButton(String title) {
+	super(title);
+	setOpaque(!Utility.isMac());
+    }
+
+    public GLIButton(String title, ImageIcon icon) {
+	super(title, icon);
+	setOpaque(!Utility.isMac());
+    }
+
+    public GLIButton(ImageIcon icon, String tooltip) {
+	super(icon);
+	setOpaque(!Utility.isMac());
+	setToolTipText(tooltip);
+    }
+    public GLIButton(String title, String tooltip) {
+	super(title);
+	setOpaque(!Utility.isMac());
+	setToolTipText(tooltip);
+    }
+
+    public GLIButton(String title, ImageIcon icon, String tooltip) {
+	super(title, icon);
+	setOpaque(!Utility.isMac());
+	setToolTipText(tooltip);
+    }
+
+   
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GProgressBar.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GProgressBar.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GProgressBar.java	(revision 31635)
@@ -0,0 +1,207 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+
+import javax.swing.*;
+import org.greenstone.gatherer.Dictionary;
+
+
+/** A progress bar that doesn't pack a sad when you feed it longs (such as the sizes of files) which get mangled into negative numbers. I know how it feels, as I often have negative progress. Also tries to be efficent when updating the JProgressBar, so only updates when the value actually changes, which become necessary when you move a large number of files with millions of bytes (where upon shifting a file of a thousand bytes would be pretty insignificant, maybe not even a percent, but would fire several progress updates). 
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ */
+public class GProgressBar
+    extends JProgressBar
+{
+    private long cur_value = 0L;
+    private long max_value = 0L;
+
+    /** The previous percentage value. */
+    private int previous = -1;
+
+
+    public GProgressBar()
+    {
+	super(0, 100);
+        this.setComponentOrientation(Dictionary.getOrientation());
+    }
+
+
+    public void addMaximum(long amount)
+    {
+	if (amount > 0) {
+	    max_value = max_value + amount;
+	    update();
+	}
+    }
+
+
+    public void addValue(long amount)
+    {
+	if (amount > 0) {
+	    cur_value = cur_value + amount;
+	    update();
+	}
+    }
+
+
+    public void clear()
+    {
+ 	previous = -1;
+ 	update();
+    }
+
+
+    public void reset()
+    {
+ 	cur_value = 0L;
+ 	max_value = 0L;
+ 	previous = -1;
+ 	update();
+    }
+
+
+    public void setIndeterminate(boolean indeterminate)
+    {
+	SwingUtilities.invokeLater(new SafeSetIndeterminateTask(indeterminate));		  
+    }
+
+
+    public void setString(String label)
+    {
+	SwingUtilities.invokeLater(new SafeSetStringTask(label));
+    }
+
+
+    public void setMaximum(int value)
+    {
+	max_value = value;
+	update();
+    }
+
+	 
+    public void setValue(int value)
+    {
+	cur_value = value;
+	update();
+    }
+
+
+    /** Only bother updating the progress bar if the percentage has changed. */
+    private void update()
+    {
+	int percentage = -1;
+	if (cur_value == 0L || max_value == 0L) {
+	    percentage = 0;
+	}
+	else {
+	    long per_value = (cur_value * 100) / max_value;
+	    percentage = (int) per_value;
+	    if (percentage > 100) {
+		percentage = 100;
+	    }
+	}
+
+	if (percentage != previous) {
+	    previous = percentage;
+	    setString(percentage + "%");
+	    SwingUtilities.invokeLater(new SafeSetValueTask(percentage));
+	}
+    }
+
+
+    /** Internal class to call JProgressBar.setIndeterminate safely. */
+    private class SafeSetIndeterminateTask
+	implements Runnable
+    {
+	private boolean indeterminate;
+
+	public SafeSetIndeterminateTask(boolean indeterminate)
+	{
+	    this.indeterminate = indeterminate;
+	}
+
+	public void run()
+	{
+	    unsafeSetIndeterminate(indeterminate);
+	}
+    }
+
+    /** This method is unsafe, -unless- it is called from the Event Queue (eg. from SafeSetIndeterminateTask). */
+    private void unsafeSetIndeterminate(boolean indeterminate)
+    {
+	super.setIndeterminate(indeterminate);
+    }
+
+
+    /** Internal class to call JProgressBar.setString safely. */
+    private class SafeSetStringTask
+	implements Runnable
+    {
+	private String label;
+
+	public SafeSetStringTask(String label)
+	{
+	    this.label = label;
+	}
+
+	public void run()
+	{
+	    unsafeSetString(label);
+	}
+    }
+
+    /** This method is unsafe, -unless- it is called from the Event Queue (eg. from SafeSetStringTask). */
+    private void unsafeSetString(String label)
+    {
+	super.setString(label);
+    }
+
+
+    /** Internal class to call JProgressBar.setValue safely. */
+    private class SafeSetValueTask
+	implements Runnable
+    {
+	private int value;
+
+	public SafeSetValueTask(int value)
+	{
+	    this.value = value;
+	}
+
+	public void run()
+	{
+	    unsafeSetValue(value);
+	}
+    }
+
+    /** This method is unsafe, -unless- it is called from the Event Queue (eg. from SafeSetValueTask). */
+    private void unsafeSetValue(int value)
+    {
+	super.setValue(value);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GUIManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GUIManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GUIManager.java	(revision 31635)
@@ -0,0 +1,1218 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.datatransfer.*;
+import java.awt.event.*;
+import java.io.File;
+import java.lang.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.filechooser.*;
+import javax.swing.plaf.*;
+import javax.swing.text.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.Collection;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.file.FileOpenActionListener;
+import org.greenstone.gatherer.file.WorkspaceTree;
+import org.greenstone.gatherer.gui.metaaudit.MetaAuditFrame;
+import org.greenstone.gatherer.gui.tree.DragTree;
+import org.greenstone.gatherer.metadata.FilenameEncoding;
+import org.greenstone.gatherer.metadata.MetadataSet;
+import org.greenstone.gatherer.metadata.MetadataXMLFileManager;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+/** The GUIManager is in charge of creating the Gatherer window frame then filling it with the goodness of the view panes. GUIManager not only creates these panes, but allows some messaging between them. Furthermore GUIManager includes functionality from menu driven choices, simply as it was easier to put it here once and have it accessible from all pane children. */
+public class GUIManager
+    extends JFrame
+    implements ActionListener, ChangeListener,  WindowFocusListener{
+    /** The download pane contains controls for downloading internet sites. */
+    public DownloadPane download_pane = null;
+    /** The gather pane is more like a file manager where you drag files from one tree to another. */
+    public GatherPane gather_pane = null;
+    /** The enrich pane is used to assign, edit and remove metadata from files within the collection. */
+    public EnrichPane enrich_pane = null;
+    /** The design pane allows you to edit the design of the library in terms of the collection configuration file. - the stuff that requires rebuilding */
+    public DesignPane design_pane = null;
+    /** The create pane contains scripting options for importing and building collections into libraries. */
+    public CreatePane create_pane = null;
+    /** The format pane allows you to edit the design of the library in terms of the collection configuration file. - the stuff that doesn't require rebuilding */
+    public FormatPane format_pane = null;
+    
+    public FileOpenActionListener foa_listener = new FileOpenActionListener();
+
+
+    /** A reference to the currently instantiated help window, if any. */
+    private HelpFrame help = null;
+    /** The menu bar. */
+    public MenuBar menu_bar = null;
+    public MetaAuditFrame meta_audit;
+    /** Are certain panes currently locked? */
+    private boolean locked = false;
+    /** The size of the Gatherer window. */
+    private Dimension size = null;
+    /** The panel within the window that other components are placed on. */
+    private JPanel content_pane = null;
+    /** The last view pane selected. */
+    private JPanel previous_pane;
+    /** The main tab pane containing the different views, available here to trap view change events. */
+    private JTabbedPane tab_pane = null;
+    /** A threaded tab changer to try and avoid NPE on exit. */
+    private TabUpdater tab_updater = null;
+
+  final static String newline = "\n";
+    final static String space = "    ";
+   
+
+    /**Constructor. Enable window events and arranges all other components.
+     * @param size The intial <strong>Dimension</strong> of the screen.
+     */
+    public GUIManager(Dimension size) {
+	super();
+        this.setComponentOrientation(Dictionary.getOrientation());
+	// Initialization
+	this.help = new HelpFrame();
+	this.size = size;
+	
+	this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+
+	// Add a focus listener to ourselves. Thus if we gain focus when a Modal Dialog should instead have it, we can try to bring the modal dialog to the fore.
+	this.addFocusListener(new GLIGUIFocusListener());
+
+	this.addWindowFocusListener(this);
+     
+	// Make the Tool tip hang around for a rediculous amount of time.
+	ToolTipManager.sharedInstance().setDismissDelay(10000);
+
+	// Set up some other UI stuff. (fonts handled in Gatherer.main())
+	UIManager.put("FileChooser.lookInLabelText", Dictionary.get("SaveCollectionBox.Look_In"));
+	UIManager.put("FileChooser.filesOfTypeLabelText", Dictionary.get("SaveCollectionBox.Files_Of_Type"));
+	UIManager.put("FileChooser.fileNameLabelText", Dictionary.get("SaveCollectionBox.File_Name"));
+
+	// JOptionPane options
+	// http://www.java2s.com/Tutorial/Java/0240__Swing/SettingJOptionPanebuttonlabelstoFrench.htm
+	UIManager.put("OptionPane.cancelButtonText", Dictionary.get("General.Cancel"));
+	UIManager.put("OptionPane.noButtonText", Dictionary.get("General.No"));
+	UIManager.put("OptionPane.okButtonText", Dictionary.get("General.OK"));
+	UIManager.put("OptionPane.yesButtonText", Dictionary.get("General.Yes"));
+    }
+
+
+    public void windowGainedFocus(WindowEvent e)
+    {
+    }
+
+
+    public void windowLostFocus(WindowEvent e)
+    {
+	// Save the loaded collection
+	if (Gatherer.c_man != null && Gatherer.c_man.ready() && Gatherer.c_man.getCollection().cdm != null) {
+	    Gatherer.c_man.saveCollection();
+	}
+    }
+
+
+    private class GLIGUIFocusListener
+	extends FocusAdapter {
+	public void focusGained(FocusEvent e) {
+            if (ModalDialog.current_modal != null) {
+		ModalDialog.current_modal.makeVisible();
+		ModalDialog.current_modal.toFront();
+	    }
+	}
+    }
+
+    /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured. In this case we are listening to actions from the menu-bar, and should react appropriately.
+     * @param event An <strong>ActionEvent</strong> containing information about the action that has occured.
+     */
+    public void actionPerformed(ActionEvent event) {
+	Object esrc = event.getSource();
+	// *************
+	// File Options.
+	// *************
+	if (esrc == menu_bar.file_associations) {
+	    Gatherer.assoc_man.edit();
+	}
+	else if (esrc == menu_bar.file_close) {
+	    saveThenCloseCurrentCollection();
+	}
+	else if (esrc == menu_bar.file_delete) {
+	    new DeleteCollectionTask().start();
+	}
+	else if (esrc == menu_bar.file_cdimage) {
+	    WriteCDImagePrompt wcdip = new WriteCDImagePrompt();
+	    wcdip.display();
+	    wcdip.destroy();
+	    wcdip = null;
+	}
+	else if (esrc == menu_bar.file_exportas) {
+	    ExportAsPrompt eap = new ExportAsPrompt();
+	    eap.display();
+	    eap.destroy();
+	    eap = null;
+	}
+	else if (esrc == menu_bar.file_exit) {
+	    exit();
+	}
+        else if (esrc == menu_bar.file_new) {
+	    new NewCollectionTask().start();
+	}
+	else if (esrc == menu_bar.file_open) {
+	    new OpenCollectionTask().start();
+	}
+	else if (esrc == menu_bar.file_options) {
+	    new Preferences();
+	}
+	else if (esrc == menu_bar.file_save) {
+	  // Very important: make sure metadata values are saved too
+	  enrich_pane.stopEditingAndRebuild();
+	  // Make sure all the metadata has been saved to file
+	  MetadataXMLFileManager.saveMetadataXMLFiles();
+	  
+	  Gatherer.c_man.saveCollection();
+	}
+
+	// *************
+	// Edit Options.
+	// *************
+	else if(esrc == menu_bar.edit_copy) {
+	    try {
+		KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
+		// Get the component with selected text as a JTextComponent
+		JTextComponent text = (JTextComponent) kfm.getPermanentFocusOwner();//getFocusOwner();
+		text.copy();
+	    }
+	    catch (Exception cce) {
+		// If the component is not a text component ignore the copy command
+		DebugStream.println(cce.toString());
+	    }
+	}
+	else if(esrc == menu_bar.edit_cut) {
+	    try {
+		KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
+		// Get the component with selected text as a JTextComponent
+		JTextComponent text = (JTextComponent) kfm.getPermanentFocusOwner();
+		// Cut the text to the clipboard
+		text.cut();
+	    }
+	    catch (ClassCastException cce) {
+		// If the component is not a text component ignore the cut command
+		DebugStream.println(cce.toString());
+	    }
+	}
+	else if(esrc == menu_bar.edit_paste) {
+	    try {
+		KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
+		// Get the component with selected text as a JTextComponent
+		JTextComponent text = (JTextComponent) kfm.getPermanentFocusOwner();
+		// Cut the text to the clipboard
+		text.paste();
+	    }
+	    catch (ClassCastException cce) {
+		// If the component is not a text component ignore the paste command
+		DebugStream.println(cce.toString());
+	    }
+	}
+	else if(esrc == menu_bar.edit_config) {	   
+	    if(Gatherer.c_man.getCollection() != null) {
+
+		ConfigFileEditor configEditor = new ConfigFileEditor();
+		configEditor.setVisible(true);
+		configEditor.setSize(900,700);
+	    }	   
+	}
+
+	// *************
+	// Help Options.
+	// *************
+	else if (esrc == menu_bar.help_general) {
+	    HelpFrame.setView("introduction");
+	}
+	else if (esrc == menu_bar.help_download) {
+	    HelpFrame.setView("themirrorview");
+	}
+	else if (esrc == menu_bar.help_gather) {
+	    HelpFrame.setView("collectingfiles");
+	}
+	else if (esrc == menu_bar.help_enrich) {
+	    HelpFrame.setView("enrichingacollection");
+	}
+	else if (esrc == menu_bar.help_design) {
+	    HelpFrame.setView("designingacollection");
+	}
+	else if (esrc == menu_bar.help_create) {
+	    HelpFrame.setView("producingthecollection");
+	}
+	else if (esrc == menu_bar.help_format) {
+	    HelpFrame.setView("formattingacollection");
+	}
+	else if (esrc == menu_bar.help_about) {
+	    new AboutDialog(this);
+	}
+    }
+
+
+    /** Any actions that should happen after the display of the Gatherer window can be called here. Currently only updates the browser pane if it is active to work around bug in Mozilla renderer implementation.
+     */
+    public void afterDisplay() {
+	if (download_pane != null) {
+	    download_pane.afterDisplay();
+	}
+	enrich_pane.afterDisplay();
+    }
+
+
+    public void closeCurrentCollection()
+    {
+	tab_pane.setSelectedComponent(gather_pane);
+	Gatherer.c_man.closeCollection();
+	FilenameEncoding.closeCollection(); // clear filename-to-encodings map
+    }
+
+
+    public void saveThenCloseCurrentCollection()
+    {
+   	Gatherer.c_man.saveCollection();
+	closeCurrentCollection();
+    }
+
+
+    /** This is called when we're absolutely finished with the GLI. It is *not* called when the applet is suspended.
+     */
+    public void destroy()
+    {
+	// Destroying create pane ensures the latest log has been closed
+	if (create_pane != null) {
+	    create_pane.destroy();
+	}
+
+	// Deal to help
+	if (help != null) {
+	    help.destroy();
+	    help = null;
+	}
+    }
+
+
+    /** Enabled events on the window to be trapped, creates all the visual components, then builds the tab and other layouts.
+     */
+    public void display() {
+	content_pane = (JPanel) this.getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+
+	// Enable window-type events to be fired.
+	enableEvents(AWTEvent.WINDOW_EVENT_MASK);
+	// Initialise and layout sub-components, plus other window dressing.
+	try {
+	    this.setSize(size);
+
+	    // Set the title
+	    String collection_title = null;
+	    String collection_name = null;
+	    if (Gatherer.c_man.ready()) {
+		Collection collection = Gatherer.c_man.getCollection();
+		collection_title = collection.getTitle();
+		collection_name = collection.getGroupQualifiedName(true);
+		collection = null;
+	    }
+	    setTitle(collection_title, collection_name);
+	    collection_title = null;
+	    collection_name = null;
+
+	    // Pretty corner icon
+	    String gsmall_image = "gatherer.png";
+	    if (Configuration.fedora_info.isActive()) {
+		gsmall_image = "fli-" + gsmall_image;
+	    }
+		if(Gatherer.isGsdlRemote) {
+	    gsmall_image = "client-" + gsmall_image;
+	    }
+	    this.setIconImage(JarTools.getImage(gsmall_image).getImage());
+	    // BorderLayout for the main screen. I'll try my best to avoid these in subcomponents as they're space greedy.
+	    content_pane.setLayout(new BorderLayout());
+	    // Create the menu-bar and stick it up the top.
+	    menu_bar = new MenuBar(new MenuListenerImpl());
+            menu_bar.setComponentOrientation(Dictionary.getOrientation());
+
+	    //feedback changes
+	    //content_pane.add(menu_bar, BorderLayout.NORTH);
+	    this.setJMenuBar(menu_bar);
+	    // end feedback changes
+
+	    // Create the tabbed pane and plop it in the center where it will
+	    // expand to consume all available space like any good gas would.
+	    tab_pane = new JTabbedPane();
+            tab_pane.setComponentOrientation(Dictionary.getOrientation());
+	    tab_pane.addChangeListener(this);
+	    tab_pane.setFont(Configuration.getFont("general.font", false));
+
+	    if (Configuration.get("workflow.download", true) && Gatherer.isDownloadEnabled) {
+		download_pane = new DownloadPane();
+		// "GUI.Download_Tooltip" is used automatically
+		tab_pane.addTab(Dictionary.get("GUI.Download"), JarTools.getImage("download.gif"), download_pane, Dictionary.get("GUI.Download_Tooltip"));
+		tab_pane.setEnabledAt(tab_pane.indexOfComponent(download_pane), Configuration.get("workflow.download", false));
+	    }
+
+	    gather_pane = new GatherPane();          
+	    gather_pane.display();
+	    if (Configuration.get("workflow.gather", true)) {
+		// "GUI.Gather_Tooltip" is used automatically
+		tab_pane.addTab(Dictionary.get("GUI.Gather"), JarTools.getImage("gather.gif"), gather_pane, Dictionary.get("GUI.Gather_Tooltip"));
+		tab_pane.setEnabledAt(tab_pane.indexOfComponent(gather_pane), Configuration.get("workflow.gather", false));
+	    }
+
+	    enrich_pane = new EnrichPane();
+	    enrich_pane.display();
+	    if (Configuration.get("workflow.enrich", true)) {
+		// "GUI.Enrich_Tooltip" is used automatically
+		tab_pane.addTab(Dictionary.get("GUI.Enrich"), JarTools.getImage("enrich.gif"), enrich_pane, Dictionary.get("GUI.Enrich_Tooltip"));
+		tab_pane.setEnabledAt(tab_pane.indexOfComponent(enrich_pane), false);
+	    }
+
+	    design_pane = new DesignPane();
+	    design_pane.display();
+	    if (Configuration.get("workflow.design", true)) {
+		// "GUI.Design_Tooltip" is used automatically
+		if (Configuration.fedora_info.isActive()) {
+		    tab_pane.addTab("Plugins", JarTools.getImage("design.gif"), design_pane, Dictionary.get("GUI.Design_Tooltip"));
+		}
+		else {
+		    tab_pane.addTab(Dictionary.get("GUI.Design"), JarTools.getImage("design.gif"), design_pane, Dictionary.get("GUI.Design_Tooltip"));
+		}
+
+		tab_pane.setEnabledAt(tab_pane.indexOfComponent(design_pane), false);
+	    }
+
+	    create_pane = new CreatePane();
+            create_pane.setComponentOrientation(Dictionary.getOrientation());
+	    create_pane.display();
+	    if (Configuration.get("workflow.create", true)) {
+		// "GUI.Create_Tooltip" is used automatically
+		tab_pane.addTab(Dictionary.get("GUI.Create"), JarTools.getImage("create.gif"), create_pane, Dictionary.get("GUI.Create_Tooltip"));
+		tab_pane.setEnabledAt(tab_pane.indexOfComponent(create_pane), false);
+	    }
+
+	    format_pane = new FormatPane();
+            format_pane.setComponentOrientation(Dictionary.getOrientation());
+	    format_pane.display();
+	    if (Configuration.get("workflow.format", true)) {
+		tab_pane.addTab(Dictionary.get("GUI.Format"), JarTools.getImage("format.gif"), format_pane, Dictionary.get("GUI.Format_Tooltip"));
+		tab_pane.setEnabledAt(tab_pane.indexOfComponent(format_pane), false);
+	    }
+	    
+	    // The MetaAuditFrame must be created after the gather/enrich panes but before they get focus
+	    meta_audit = new MetaAuditFrame();
+            meta_audit.setComponentOrientation(Dictionary.getOrientation());
+	    // Select the collect pane if it is available
+	    if (tab_pane.indexOfComponent(gather_pane) != -1) {
+		tab_pane.setSelectedComponent(gather_pane);
+	    }
+	    // Otherwise find the first tab that is enabled and select that.
+	    else {
+		for (int i = 0; i < tab_pane.getTabCount(); i++) {
+		    if (tab_pane.isEnabledAt(i)) {
+			tab_pane.setSelectedIndex(i);
+			break;
+		    }
+		}
+	    }
+
+	    content_pane.add(tab_pane, BorderLayout.CENTER);
+
+	    // Add an extra progress bar at the bottom of every screen when using a remote Greenstone server
+	    if (Gatherer.isGsdlRemote) {
+		JPanel remote_greenstone_server_progress_panel = new JPanel();
+               //remote_greenstone_server_progress_panel.setComponentOrientation(Dictionary.getOrientation());
+		JLabel remote_greenstone_server_progress_label = new JLabel(Dictionary.get("RemoteGreenstoneServer.Progress"));
+                //remote_greenstone_server_progress_label.setComponentOrientation(Dictionary.getOrientation());
+		remote_greenstone_server_progress_panel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+		remote_greenstone_server_progress_panel.setLayout(new BorderLayout());
+		remote_greenstone_server_progress_panel.add(remote_greenstone_server_progress_label, BorderLayout.LINE_START);
+		remote_greenstone_server_progress_panel.add(Gatherer.remoteGreenstoneServer.getProgressBar(), BorderLayout.CENTER);
+		content_pane.add(remote_greenstone_server_progress_panel, BorderLayout.SOUTH);
+	    }
+
+	    // Call refresh to update all controls to reflect current collection status.
+	    refresh(-1, Gatherer.c_man.ready());
+	}
+	catch (Exception e) {
+	    DebugStream.printStackTrace(e);
+	    // The GUI failing to build is an app killer
+	    e.printStackTrace();
+	    System.exit(1);
+	}
+    }
+
+    public void exit() 
+    {
+	exit(0);
+    }
+	
+  // some cases we have already saved the collection, but don't want to 
+  // override the general.open_collection value here
+  public void exitNoCollectionSave(int exit_status) {
+	// Store the current position and size of the GLI for next time
+	Configuration.setBounds("general.bounds", true, getBounds());
+
+	// Save configuration
+	Configuration.save();
+
+	// Hide the main window
+	setVisible(false);
+
+	// If we're running as an applet we don't quit here (we quit when the browser calls GathererApplet.destroy())
+	if (!Gatherer.isApplet) {
+	    Gatherer.exit(exit_status);
+	}
+
+  }
+    /** This method ensures that all the things needing saving are saved before Gatherer.exit() is called.
+     */
+    public void exit(int exit_status)
+    {
+	boolean collection_open = false;
+	// If we have a collection open remember it for next time, then save it and close it
+	if (Gatherer.c_man.ready()) {
+		// but we no longer remember open collections for remote GLI, as this causes problems
+		// in applets when other users on a machine want to use an applet and GLI wants to load
+		// a collection left open from a previous session and *requires* the old user to authenticate
+		if(Gatherer.isGsdlRemote) {
+			Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
+				    true, "");			
+		} else {
+			Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
+				    true, CollectionManager.getLoadedCollectionColFilePath());
+		}
+	    saveThenCloseCurrentCollection();
+		collection_open = true;
+	}
+	else {
+	    // if there was no open collection, then write out the collect dir path for next time
+		// IF this is not the default collect directory and not a remote GLI/applet
+		if(Gatherer.isGsdlRemote || Gatherer.getCollectDirectoryPath().equals(
+				Gatherer.getDefaultGSCollectDirectoryPath(true))) {	
+			Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
+					true, "");
+		} else {
+			Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
+					true, Gatherer.getCollectDirectoryPath());
+		}
+	}
+	
+	
+	// Basically, if GLI coldir != gsdlsite_colhome, we ask them if they want to save GLI coldir as gsdlsite_colhome
+	// If gsdlsite_colhome = "" (meaning default GS coldir) and GLI is also set to defaultGScoldir, no saving necessary.
+	// We ONLY do this for the local included apache web server, since the issue revolves around gsdlsite.cfg containing
+	// the global collecthome when using the apache web server. This is not an issue for remote GS or server.exe (the latter
+	// has separate config files specifying collecthome for when GLI is running whereas when just the server.exe is running).
+	if(!Gatherer.isGsdlRemote && !Gatherer.isPersistentServer()) {		
+	
+		String defaultColDir = Gatherer.getDefaultGSCollectDirectoryPath(false); // no filesep at end
+		String gliColDir = Gatherer.getCollectDirectoryPath();
+		String serverColDir = Gatherer.gsdlsite_collecthome;
+		
+		boolean showSettingsDlg = true;
+		// if collectdir was changed during GLI session (to something different from what's in gsdlsite), need to ask what to store
+		String gsdlsiteColdir = Gatherer.gsdlsite_collecthome.replace("\"", "");
+		if(gliColDir.equals(gsdlsiteColdir+File.separator)) {	
+			showSettingsDlg = false;
+		} else if(gsdlsiteColdir.equals("") // both set to default coldir
+				&& gliColDir.equals(defaultColDir+File.separator)) {
+			showSettingsDlg = false;
+		} 
+		if(showSettingsDlg) {
+			
+			// else we will be showing the Collect Directory Settings Dialog
+			if(gliColDir.endsWith(File.separator)) {
+				gliColDir = gliColDir.substring(0, gliColDir.length()-1);
+			}
+			
+			if(serverColDir.equals("")) {
+				serverColDir = defaultColDir;
+			}	
+			collectDirSettingsDialog(defaultColDir, serverColDir, gliColDir, collection_open);
+		}
+	}
+
+	// Store the current position and size of the GLI for next time
+	Configuration.setBounds("general.bounds", true, getBounds());
+
+	// Save configuration
+	Configuration.save();
+
+	// Hide the main window
+	setVisible(false);
+
+	// If we're running as an applet we don't quit here (we quit when the browser calls GathererApplet.destroy())
+	if (!Gatherer.isApplet) {
+	    Gatherer.exit(exit_status);
+	}
+    }
+
+	public void collectDirSettingsDialog(final String defaultColDir,
+		final String from, final String to, final boolean collection_open) 
+	{	
+		final JDialog colHomeDialog 
+			= new JDialog(this, Dictionary.get("GUI.CollectHome.title"), true); // this = Gatherer.g_man
+		colHomeDialog.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+		
+		
+		JRadioButton gliYes = new JRadioButton(Dictionary.get("General.Yes"), true);
+						// the default selection for GLI, collecthome=to
+		JRadioButton serverYes = new JRadioButton(Dictionary.get("General.Yes")); // not selected for the server		
+		JRadioButton gliNo = null;
+		JRadioButton gliThirdOption = null;
+		JRadioButton serverNo = null;
+		JRadioButton serverThirdOption = null;
+		
+		if(from.equals(defaultColDir)) { 
+			gliNo = new JRadioButton(Dictionary.get("GUI.CollectHome.resetToDefault"));
+			serverNo = new JRadioButton(Dictionary.get("GUI.CollectHome.leaveAtDefault"), true);
+						// default selection for server, collecthome=from
+		} else {
+			gliNo = new JRadioButton(Dictionary.get("General.No"));
+			serverNo = new JRadioButton(Dictionary.get("General.No"), true); // default selection for server 
+			if(!to.equals(defaultColDir)) { // neither from nor to is the default GS collect dir, so give them that as the third option
+				gliThirdOption = new JRadioButton(Dictionary.get("GUI.CollectHome.reset"));
+				serverThirdOption = new JRadioButton(Dictionary.get("GUI.CollectHome.reset"));		
+			}
+		}
+		
+		JPanel gliPanel = new JPanel(); // flowlayout by default
+		ButtonGroup gliGroup = new ButtonGroup();
+		JPanel serverPanel = new JPanel(); // flowlayout by default
+		ButtonGroup serverGroup = new ButtonGroup();
+		
+		gliGroup.add(gliYes);
+		gliPanel.add(gliYes);
+		serverGroup.add(serverYes);
+		serverPanel.add(serverYes);		
+		
+		gliGroup.add(gliNo);
+		gliPanel.add(gliNo);
+		serverGroup.add(serverNo);
+		serverPanel.add(serverNo);
+			
+		if(gliThirdOption != null) {
+			gliThirdOption = new JRadioButton("Reset to default");
+			serverThirdOption = new JRadioButton("Reset to default");
+			
+			gliGroup.add(gliThirdOption);
+			gliPanel.add(gliThirdOption);
+			serverGroup.add(serverThirdOption);
+			serverPanel.add(serverThirdOption);
+		}
+		
+		// vars need to be final to use them in the actionlistener below
+		final JRadioButton gli_yes = gliYes;
+		final JRadioButton gli_no = gliNo;
+		final JRadioButton gli_optional = gliThirdOption;
+		final JRadioButton server_yes = serverYes;
+		final JRadioButton server_no = serverNo;
+		final JRadioButton server_optional = serverThirdOption;
+		
+		JButton okButton = new JButton(Dictionary.get("General.OK"));
+		okButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				// store the option chosen for GLI
+				String gliColDir = to;	
+				if(gli_optional != null && gli_optional.isSelected()) {
+					// defaultColDir
+					gliColDir = "";
+				} else if(gli_yes.isSelected()) {
+					gliColDir = to;
+				} else if (gli_no.isSelected()) {
+					gliColDir = from;
+				}
+				if(defaultColDir.equals(gliColDir)) {
+					gliColDir = "";
+				}
+				if(!(collection_open && gli_yes.isSelected())) { // don't overwrite open collections
+					Configuration.setString(
+						"general.open_collection"+Configuration.gliPropertyNameSuffix(), 
+						true, gliColDir);
+				}
+				
+				// store the option chosen for the server's settings
+				String serverColDir = from;
+				if(server_optional != null && server_optional.isSelected()) {
+					// defaultColDir
+					serverColDir = null;
+				} else if(server_yes.isSelected()) {
+					serverColDir = to;
+				} else if (server_no.isSelected()) {
+					serverColDir = from;
+				}
+				if(serverColDir != null && defaultColDir.equals(serverColDir)) {					
+					serverColDir = null;
+				}				
+				
+				if(serverColDir != null) {
+					serverColDir = serverColDir.replace("\"","");
+					serverColDir = "\""+serverColDir+"\"";
+				}
+								
+				Gatherer.gsdlsite_collecthome = Utility.updatePropertyConfigFile(
+					Gatherer.getGsdlSiteConfigFile(), "collecthome", serverColDir);	
+				
+				colHomeDialog.dispose();
+			}
+		});
+		
+		//String[] dirs = {from, to};		
+		//JLabel message = new JLabel(Dictionary.get("GUI.CollectHome.message", dirs));
+		JLabel message = new JLabel(Dictionary.get("GUI.CollectHome.message"));
+		JLabel fromDirLine = new JLabel(Dictionary.get("GUI.CollectHome.dir", from));
+		fromDirLine.setBorder(BorderFactory.createEmptyBorder(0,25,0,0)); // padding
+		JLabel toLine = new JLabel(Dictionary.get("GUI.CollectHome.to"));
+		JLabel toDirLine = new JLabel(Dictionary.get("GUI.CollectHome.dir", to));
+		toDirLine.setBorder(BorderFactory.createEmptyBorder(0,25,0,0)); // padding
+		JLabel gliOption = new JLabel(Dictionary.get("GUI.CollectHome.gli"));
+		JLabel serverOption = new JLabel(Dictionary.get("GUI.CollectHome.server"));		
+		
+		JPanel mainPanel = new JPanel();
+		mainPanel.setLayout(new GridLayout(9, 1));
+		mainPanel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); // padding
+		mainPanel.add(message);
+		mainPanel.add(fromDirLine);		
+		mainPanel.add(toLine);
+		mainPanel.add(toDirLine);
+		mainPanel.add(gliOption);
+		mainPanel.add(gliPanel);
+		mainPanel.add(serverOption);
+		mainPanel.add(serverPanel);
+		mainPanel.add(okButton);		
+		Container c = colHomeDialog.getContentPane();
+		c.setLayout(new BorderLayout());
+		c.add(mainPanel, BorderLayout.CENTER);
+		colHomeDialog.getRootPane().setDefaultButton(okButton);
+		
+		colHomeDialog.pack();
+		colHomeDialog.setVisible(true);
+	}
+	
+
+    /** This method is called when the collection is being built, and is used to disable all controls in all pane which could change the state of the collection.
+     */
+    public void lockCollection(boolean import_stage, boolean lock)
+    {
+	locked = lock;
+
+	if (import_stage) {
+	    int gather_pos = tab_pane.indexOfComponent(gather_pane);
+	    if (gather_pos != -1) {
+		tab_pane.setEnabledAt(gather_pos, !lock);
+	    }
+	    int enrich_pos = tab_pane.indexOfComponent(enrich_pane);
+	    if (enrich_pos != -1) {
+		tab_pane.setEnabledAt(enrich_pos, !lock);
+	    }
+	}
+
+	int design_pos = tab_pane.indexOfComponent(design_pane);
+	if (design_pos != -1) {
+	    tab_pane.setEnabledAt(design_pos, !lock);
+	}
+    }
+
+
+    public void modeChanged(int mode) {
+	// Set the title
+	String collection_title = null;
+	String collection_name = null;
+	if (Gatherer.c_man.ready()) {
+	    Collection collection = Gatherer.c_man.getCollection();
+	    collection_title = collection.getTitle();
+	    collection_name = collection.getGroupQualifiedName(true);
+	    collection = null;
+	}
+	setTitle(collection_title, collection_name);
+	collection_title = null;
+	collection_name = null;
+	// Now pass on the message to anyone who cares
+	if (download_pane != null) {
+	    download_pane.modeChanged(mode);
+	}
+	if (gather_pane != null) {
+	    gather_pane.modeChanged(mode);
+	}
+	if (enrich_pane != null) {
+	    enrich_pane.modeChanged(mode);
+	}
+	if (design_pane != null) {
+	    design_pane.modeChanged(mode);
+	}
+	if (create_pane != null) {
+	    create_pane.modeChanged(mode);
+	}
+	if (format_pane != null) {
+	    format_pane.modeChanged(mode);
+	}
+    }
+
+
+    public void refresh(int refresh_reason, boolean collection_loaded)
+    {
+	// Set the collection information in the title bar
+	if (collection_loaded) {
+	    Collection collection = Gatherer.c_man.getCollection();
+	    setTitle(collection.getTitle(), collection.getGroupQualifiedName(true));
+	}
+	else {
+	    setTitle(null, null);
+	}
+
+	// Update the menu bar
+	menu_bar.refresh(refresh_reason, collection_loaded);
+
+	// Update the loaded panes
+	if (download_pane != null) {
+	    download_pane.refresh(refresh_reason, collection_loaded);
+	}
+	if (gather_pane != null) {
+	    gather_pane.refresh(refresh_reason, collection_loaded);
+	}
+	if (enrich_pane != null) {
+	    enrich_pane.refresh(refresh_reason, collection_loaded);
+	}
+	if (design_pane != null) {
+	    design_pane.refresh(refresh_reason, collection_loaded);
+	}
+	if (create_pane != null) {
+	    create_pane.refresh(refresh_reason, collection_loaded);
+	}
+	if (format_pane != null) {
+	    format_pane.refresh(refresh_reason, collection_loaded);
+	}
+
+	// Now enable tabs as necessary. Do this on event queue to prevent crazy NPEs
+	if (!locked) {
+	    if (tab_updater == null) {
+		tab_updater = new TabUpdater(tab_pane, collection_loaded);
+	    }
+	    else {
+		tab_updater.setReady(collection_loaded);
+	    }
+	    SwingUtilities.invokeLater(tab_updater);
+	}
+    }
+
+
+    public void refreshCollectionTree(int refresh_reason)
+    {
+	if (gather_pane != null) {
+	    gather_pane.refreshCollectionTree(refresh_reason);
+	}
+    }
+
+
+    public void refreshWorkspaceTree(int refresh_reason)
+    {
+	if (gather_pane != null) {
+	    gather_pane.refreshWorkspaceTree(refresh_reason);
+	}
+    }
+
+    public void refreshWorkspaceTreeGreenstoneCollections()
+    {
+	refreshWorkspaceTree(WorkspaceTree.LIBRARY_CONTENTS_CHANGED);
+    }
+
+    /** Specifies whether a certain tab is enabled or not. */
+    private void setTabEnabled(String rawname, boolean state) {
+	// Retrieve the dictionary based name.
+	String name = Dictionary.get("GUI." + rawname);
+	int index = tab_pane.indexOfTab(name);
+	// Of course we may not have this tab available.
+	if(index != -1) {
+	    // Some tabs are also dependant on if a collection is ready
+	    Component component = tab_pane.getComponentAt(index);
+	    if(component == enrich_pane || component == design_pane || component == create_pane || component == format_pane) {
+		tab_pane.setEnabledAt(index, state && Gatherer.c_man != null && Gatherer.c_man.ready());
+	    }
+	    else {
+		tab_pane.setEnabledAt(index, state);
+	    }
+	    // If this was the currently selected tab and it is now disabled, change the view to the first enabled tab.
+	    if(tab_pane.getSelectedIndex() == index && !state) {
+		boolean found = false;
+		for(int i = 0; !found && i < tab_pane.getTabCount(); i++) {
+		    if(tab_pane.isEnabledAt(i)) {
+			tab_pane.setSelectedIndex(i);
+			found = true;
+		    }
+		}
+		// If there are no tabs enabled, which should be impossible, then select the first tab
+		if(!found) {
+		    tab_pane.setSelectedIndex(0);
+		}
+	    }
+	}
+    }
+
+    /** Change the string shown in the title bar of the main gui frame. If either value is null, the 'No Collection' string is shown instead.
+     * @param title
+     * @param name
+     */
+    public void setTitle(String title, String name) {
+      
+	// Finally display the collection name in the title bar.
+	StringBuffer title_buffer = new StringBuffer(Configuration.getApplicationTitle());
+	title_buffer.append(StaticStrings.SPACE_CHARACTER);
+	title_buffer.append(Gatherer.PROGRAM_VERSION);
+	title_buffer.append(StaticStrings.SPACE_CHARACTER);
+	title_buffer.append(StaticStrings.SPACE_CHARACTER);
+	// Server version information
+	title_buffer.append(Gatherer.getServerVersionAsString());
+	title_buffer.append(StaticStrings.SPACE_CHARACTER);
+	title_buffer.append(StaticStrings.SPACE_CHARACTER);	
+	// Describe the current user mode
+	title_buffer.append(StaticStrings.MODE_STR);
+	title_buffer.append(Configuration.getModeAsString());
+	title_buffer.append(StaticStrings.SPACE_CHARACTER);
+	title_buffer.append(StaticStrings.SPACE_CHARACTER);
+	// Now for the current collection
+	title_buffer.append(StaticStrings.COLLECTION_STR);
+	if (title != null && name != null) {
+	    title_buffer.append(title);
+	    title_buffer.append(StaticStrings.SPACE_CHARACTER);
+	    title_buffer.append(StaticStrings.OPEN_PARENTHESIS_CHARACTER);
+	    title_buffer.append(name);
+	    title_buffer.append(StaticStrings.CLOSE_PARENTHESIS_CHARACTER);
+	}
+	else {
+	    title_buffer.append(Dictionary.get("Collection.No_Collection"));
+	}
+	this.setTitle(title_buffer.toString());
+	title_buffer = null;
+    }
+
+
+    private class OpenCollectionTask
+        extends Thread
+    {
+	public void run()
+	{
+	    String collection_file_path = showOpenCollectionDialog();
+
+	    // User has selected a collection to open
+	    if (collection_file_path != null) {
+		// If there is already a collection open, save and close it
+		if (Gatherer.c_man.ready()) {
+		    saveThenCloseCurrentCollection();
+		}
+
+		// Open the selected collection
+		Gatherer.c_man.loadCollection(collection_file_path);
+	    }
+	}
+    }
+
+
+    /** When the load collection option is chosen this method is called to produce the modal file load prompt.
+     */
+    private String showOpenCollectionDialog()
+    {
+	OpenCollectionDialog dialog = new OpenCollectionDialog();
+        dialog.setComponentOrientation(Dictionary.getOrientation());
+	if (dialog.display() == OpenCollectionDialog.OK_OPTION) {
+	    return dialog.getFileName();
+	}
+
+	// User must have cancelled the action
+	return null;
+    }
+
+
+    /** When called this method causes the MetadataAuditTable to display a nice dialog box which contains all the metadata assigned in the collection.
+     */
+    public void showMetaAuditBox() {
+	wait(true);
+	meta_audit.display();
+	wait(false);
+    }
+
+
+    private class NewCollectionTask
+	extends Thread
+    {
+	public void run()
+	{
+	    // Create the collection details prompt from new collection prompt
+	    NewCollectionDetailsPrompt ncd_prompt = new NewCollectionDetailsPrompt();
+
+	    // Create the new collection (if not cancelled) in a new thread.
+	    if (!ncd_prompt.isCancelled()) {
+		// If there is already a collection open, save and close it.
+		if (Gatherer.c_man.ready()) {
+		    saveThenCloseCurrentCollection();
+		}
+		
+		// Create new collection.
+		Gatherer.c_man.createCollection(ncd_prompt.getDescription(), Configuration.getEmail(), ncd_prompt.getName(), ncd_prompt.getTitle(), ncd_prompt.getBase(), new ArrayList());
+		ncd_prompt.dispose();
+	    }
+
+	    // Done
+	    ncd_prompt = null;
+	}
+    }
+
+
+    private class DeleteCollectionTask
+	extends Thread
+    {
+	public void run()
+	{
+	    // The rest is handled by the DeleteCollectionPrompt
+	    DeleteCollectionPrompt dc_prompt = new DeleteCollectionPrompt();
+	    if (dc_prompt.display()) {
+		//closeCurrentCollection();
+	    }
+	    dc_prompt.destroy();
+	    dc_prompt = null;
+	}
+    }
+
+
+    /** Any implementation of ChangeListener must include this method so we can be informed when the state of one of the registered objects changes. In this case we are listening to view changes within the tabbed pane.
+     * @param event A ChangeEvent containing information about the event that fired this call.
+     */
+    public void stateChanged(ChangeEvent event)
+    {
+	if (previous_pane != null) {
+	    if (previous_pane == gather_pane) {
+		gather_pane.loseFocus();
+	    }
+	    else if (previous_pane == enrich_pane) {
+		enrich_pane.loseFocus();
+	    }
+	    else if (previous_pane == design_pane) {
+		design_pane.loseFocus();
+	    }
+	    else if (previous_pane == create_pane) {
+		create_pane.loseFocus();
+	    }
+	    else if (previous_pane == format_pane) {
+		format_pane.loseFocus();
+	    }
+	}
+
+	menu_bar.tabSelected(tab_pane.getSelectedIndex());
+	int selected_index = tab_pane.getSelectedIndex();
+	if (selected_index == tab_pane.indexOfComponent(download_pane)) {
+	    download_pane.gainFocus();
+	}
+	else if (selected_index == tab_pane.indexOfComponent(gather_pane)) {
+	    gather_pane.gainFocus();
+	}
+	else if (selected_index == tab_pane.indexOfComponent(enrich_pane)) {
+	    enrich_pane.gainFocus();
+	}
+	else if (selected_index == tab_pane.indexOfComponent(design_pane)) {
+	    design_pane.gainFocus();
+	}
+	else if (selected_index == tab_pane.indexOfComponent(create_pane)) {
+	    create_pane.gainFocus();
+	}
+	else if (selected_index == tab_pane.indexOfComponent(format_pane)) {
+	    format_pane.gainFocus();
+	}
+
+	previous_pane = (JPanel) tab_pane.getSelectedComponent();
+	
+    }
+
+
+    private MouseListener mouse_blocker_listener = new MouseAdapter() {};
+
+    public void updateUI()
+    {
+	JPanel pane = (JPanel) getContentPane();
+	pane.updateUI();
+	// Also update all of the tabs according to workflow.
+	workflowUpdate("Download", Configuration.get("workflow.download", false));
+	workflowUpdate("Gather", Configuration.get("workflow.gather", false));
+	workflowUpdate("Enrich", Configuration.get("workflow.enrich", false));
+	workflowUpdate("Design", Configuration.get("workflow.design", false));
+	workflowUpdate("Create", Configuration.get("workflow.create", false));
+	workflowUpdate("Format", Configuration.get("workflow.format", false));
+    }
+
+    public void wait(boolean waiting) {
+	Component glass_pane = getGlassPane();
+	if(waiting) {
+	    // Show wait cursor.
+	    glass_pane.addMouseListener(mouse_blocker_listener);
+	    glass_pane.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+	    glass_pane.setVisible(true);
+	}
+	else {
+	    // Hide wait cursor.
+	    glass_pane.setVisible(false);
+	    glass_pane.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+	    glass_pane.removeMouseListener(mouse_blocker_listener);
+	}
+	glass_pane = null;
+    }
+
+    public void workflowUpdate(String raw, boolean state) {
+	WorkflowUpdater task = new WorkflowUpdater(raw, state);
+	SwingUtilities.invokeLater(task);
+	task = null;
+    }
+
+
+    /**Overridden from JFrame so we can exit safely when window is closed (or destroyed).
+     * @param event A <strong>WindowEvent</strong> containing information about the event that fired this call.
+     */
+    protected void processWindowEvent(WindowEvent event) {
+	if(event.getID() == WindowEvent.WINDOW_CLOSING) {
+	    exit();
+	}
+    }
+
+
+    /** Listens to actions upon the menu bar, and if it detects a click over the help menu brings the help window to the front if it has become hidden.
+     */
+    private class MenuListenerImpl
+	implements MenuListener {
+	/** Called whenever a popup menu is hidden, but we don't care.
+	 * @param e Some <strong>MenuEvent</strong> that we could care less about.
+	 */
+	public void menuCanceled(MenuEvent e) {
+	}
+	/** Called whenever a menu header (ie button) becomes unselected, but we don't care.
+	 * @param e Some <strong>MenuEvent</strong> that we could care less about.
+	 */
+	public void menuDeselected(MenuEvent e) {
+	}
+	/** This method, when a menu is first opened, is the only one we respond to by bringing the help window to the front if possible, but only if there is a help window and the help menu is the one opening.
+	 * @param e The <strong>MenuEvent</strong> whose source is checked.
+	 */
+	public void menuSelected(MenuEvent e) {
+	    if(e.getSource() == menu_bar.help) {
+		if(menu_bar.help.isSelected()) {
+		    menu_bar.help.doClick(10);
+		}
+	    }
+	    else if(e.getSource() == menu_bar.edit) { // someone clicked the toplevel Edit menu
+		// gray out the Edit Config File menu if there's no collection currently open.
+		if(Gatherer.c_man.getCollection() == null) {
+		    menu_bar.edit_config.setEnabled(false);
+		}
+		else { // don't forget to reenable the Edit ConfigXML menu item when applicable
+		    menu_bar.edit_config.setEnabled(true);
+		}
+	    }
+
+	}
+    }
+
+    private class TabUpdater
+	implements Runnable {
+	private boolean ready = false;
+	private int download_pos = -1;
+	private int enrich_pos = -1;
+	private int design_pos = -1;
+	private int create_pos = -1;
+	private int format_pos = -1;
+	private int export_pos = -1;
+	private JTabbedPane tab_pane = null;
+
+	public TabUpdater(JTabbedPane tab_pane, boolean ready) {
+	    this.ready = ready;
+	    this.tab_pane = tab_pane;
+	    download_pos = tab_pane.indexOfComponent(download_pane);
+	    enrich_pos = tab_pane.indexOfComponent(enrich_pane);
+	    design_pos = tab_pane.indexOfComponent(design_pane);
+	    create_pos = tab_pane.indexOfComponent(create_pane);
+	    format_pos = tab_pane.indexOfComponent(format_pane);
+	}
+
+	public void run()
+	{
+	    if (download_pos != -1) {
+		if (ready) {
+		    tab_pane.setEnabledAt(download_pos, Configuration.get("workflow.download", false));
+		}
+		else {
+		    tab_pane.setEnabledAt(download_pos, Configuration.get("workflow.download", true));
+		}
+	    }
+	    if (enrich_pos != -1) {
+		tab_pane.setEnabledAt(enrich_pos, ready && Configuration.get("workflow.enrich", false));
+	    }
+	    if (design_pos != -1) {
+		tab_pane.setEnabledAt(design_pos, ready && Configuration.get("workflow.design", false) && Configuration.getMode() > Configuration.ASSISTANT_MODE);
+	    }
+	    if (create_pos != -1) {
+		tab_pane.setEnabledAt(create_pos, ready && Configuration.get("workflow.create", false));
+	    }
+	    if (format_pos != -1) {
+		tab_pane.setEnabledAt(format_pos, ready && Configuration.get("workflow.format", false) && Configuration.getMode() > Configuration.ASSISTANT_MODE);
+	    }
+	}
+
+	public void setReady(boolean ready) {
+	    this.ready = ready;
+	}
+    }
+
+    private class WorkflowUpdater
+	implements Runnable {
+	private boolean state;
+	private String raw;
+	public WorkflowUpdater(String raw, boolean state) {
+	    this.raw = raw;
+	    this.state = state;
+	}
+	public void run() {
+	    setTabEnabled(raw, state);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GUIUtils.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GUIUtils.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GUIUtils.java	(revision 31635)
@@ -0,0 +1,69 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+
+public class GUIUtils {
+
+    /** The name of the mouse listener that initiates editing on a double click. */
+    static final private String SINGLE_CLICK_LISTENER = "SingleClickListener";
+
+    /** Neat method to disable file renaming in filechooser.
+     * Thanks to: vladi21 from www.experts-exchange.com
+     */
+    static public void disableRename(Component c) {
+	if (c instanceof JList){
+	    EventListener[] listeners=c.getListeners(MouseListener.class);
+	    for(int i=0; listeners != null && i < listeners.length; i++) {
+		if (listeners[i].toString().indexOf(SINGLE_CLICK_LISTENER) != -1) {
+		    c.removeMouseListener((MouseListener)listeners[i]);
+		}
+	    }
+	    return;
+	}
+	if (c instanceof Container) {
+	    Component[] children = null;
+	    children = ((Container)c).getComponents();
+	    if (children != null) {
+		for(int i = 0; children != null && i < children.length; i++) {
+		    disableRename(children[i]);
+		}
+	    }
+	}
+    }
+
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GatherPane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GatherPane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/GatherPane.java	(revision 31635)
@@ -0,0 +1,442 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionTree;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.file.FileNode;
+import org.greenstone.gatherer.file.FileOpenActionListener;
+import org.greenstone.gatherer.file.FileQueue;
+import org.greenstone.gatherer.file.FileSystemModel;
+import org.greenstone.gatherer.file.RecycleBin;
+import org.greenstone.gatherer.file.WorkspaceTree;
+import org.greenstone.gatherer.file.WorkspaceTreeNode;
+import org.greenstone.gatherer.gui.tree.DragTree;
+import org.greenstone.gatherer.util.DragComponent;
+import org.greenstone.gatherer.util.DragGroup;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.Utility;
+
+
+/** The collection pane is analogous with a file manager. It is there that the user chooses which files to include in their collection and what structure the file hierarchy should take. The later aspect is not important for the Greenstone Suite, but is usefull for grouping files for ease of metadata markup. The view essentially consists of two file trees, one denoting the entire source workspace and the other the files within your collection. The trees themselves have a title bar at the top, a filter control at the bottom, and are coloured to indicate activity (grey for disabled). The remainder of the screen is taken by a status area, to indicate current file job progress during copying etc, and three buttons for controlling features of the view.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class GatherPane
+    extends JPanel
+    implements ActionListener
+{
+    private CardLayout card_layout = null;
+    private JPanel card_pane = null;
+    /** The name of the panel containing the "collection loaded" (collection tree) card. */
+    private String COLLECTION_LOADED_CARD = "";
+    /** The name of the panel containing the "no collection loaded" placeholder */
+    private String NO_COLLECTION_LOADED_CARD = "No collection loaded";
+    /** The group encompassing all of the components available as drop targets for drag and drop actions. Required so that only one component renders the ghost and higlights itself as a target, which the other members are restored to their original, pristine, condition. */
+    private DragGroup group = null;
+    /** The tree showing the files within the collection. */
+    private CollectionTree collection_tree = null;
+    /** The threaded queue that handles the actually movement of files, so that the gui remains responsive. */
+    private FileQueue file_queue = null;
+    /** The filter currently applied to the collection tree. */
+    private Filter collection_filter = null;
+    /** The filter currently applied to the workspace tree. */
+    private Filter workspace_filter = null;
+    /** The button used to cancel all pending file queue jobs. */
+    private JButton stop_action =  null;
+    /** The button used to create a new folder in the collection tree. */
+    private JButton new_folder =  null;
+    /** The label shown at the top of the collection tree. */
+    private JLabel collection_label = null;
+    /** The label shown in the status area explaining the file apon which action is taking place. */
+    private JLabel filename_label = null;
+    /** The label shown explaining the current state of the file queue thread. */
+    private JLabel status_label	= null;
+    /** The label at the top of the workspace tree. */
+    private JLabel workspace_label = null;
+    /** The panel that contains the collection tree. */
+    private JPanel collection_pane = null;
+    /** The panel that contains the various controls including the status area. */
+    private JPanel control_pane = null;
+    /** The panel that contains the workspace tree. */
+    private JPanel workspace_pane = null;
+    /** The scrollable area into which the collection tree is placed. */
+    private JScrollPane collection_scroll = null;
+    /** The scrollable area into which the workspace tree is placed. */
+    private JScrollPane workspace_scroll = null;
+    /** A split pane seperating the two trees, allowing for the screen real-estate for each to be changed. */
+    private JSplitPane tree_pane = null;
+    /** The minimum size a gui component can become. */
+    static private Dimension MIN_SIZE = new Dimension( 90,  90);
+    /** The default size of the status area. */
+    static private Dimension STATUS_SIZE = new Dimension(450, 120);
+    /** The initial size of the trees. */
+    static private Dimension TREE_SIZE = new Dimension(400, 430);
+
+    /** The tree showing the available source workspace. */
+    public WorkspaceTree workspace_tree = null;
+
+
+    public GatherPane()
+    {
+	this.group = new DragGroup();
+	this.file_queue = Gatherer.f_man.getQueue();
+        this.setComponentOrientation(org.greenstone.gatherer.Dictionary.getOrientation());
+	// Create components.
+	stop_action = new GLIButton(Dictionary.get("Collection.Stop"), Dictionary.get("Collection.Stop_Tooltip"));
+	stop_action.addActionListener(this);
+	stop_action.setEnabled(false);
+	file_queue.registerStopButton(stop_action);
+		
+	new_folder = new GLIButton(JarTools.getImage("folder.gif"), Dictionary.get("Collection.New_Folder_Tooltip"));
+	new_folder.addActionListener(this);
+	new_folder.setEnabled(false);
+	new_folder.setMinimumSize(MIN_SIZE);
+	new_folder.setPreferredSize(MIN_SIZE);
+	
+    }
+
+    /** Any implementation of ActionListener requires this method so that when an action is performed the appropriate effect can occur. In this case there are three valid possibilities. If the action occured on the recycle bin, then delete the current selection from the collection tree. If the action instead occured on the new folder button, then create a new folder under the current (single) selection in the collection tree. And finally if the cancel button was pressed, cancel the current, and remaining, jobs on the file queue. */
+    public void actionPerformed(ActionEvent event) {
+	// If a user has clicked on the bin button directly remove whatever
+	// files are selected in the active tree.
+	if (event.getSource() == Gatherer.recycle_bin) {
+	    if (!Gatherer.recycle_bin.ignore()) {
+		// Find the active tree (you've made selections in).
+		DragTree tree = (DragTree) group.getActive();
+		if (tree != null) { // if something has indeed been selected
+		    // Fudge things a bit
+		    group.setSource(tree);
+		    // Determine the selection.
+		    TreePath paths[] = tree.getSelectionPaths();
+		    if(paths != null) {
+			FileNode[] source_nodes = new FileNode[paths.length];
+			for(int i = 0; i < paths.length; i++) {
+			    source_nodes[i] = (FileNode)(paths[i].getLastPathComponent());
+			}
+			Gatherer.f_man.action(tree, source_nodes, Gatherer.recycle_bin, null);
+		    }
+		}
+	    }
+	}
+	// If a user has clicked on new_folder create a new folder under
+	// whatever node is selected.
+	else if(event.getSource() == new_folder && collection_tree != null) {
+	    int count = collection_tree.getSelectionCount();
+	    boolean error = false;
+	    if(count == 1) {
+		TreePath path = collection_tree.getSelectionPath();
+		CollectionTreeNode node = (CollectionTreeNode) path.getLastPathComponent();
+		if (node.getAllowsChildren()) {
+		    Gatherer.f_man.newFolder(collection_tree, node);
+		}
+		else {
+		    // try the parent
+		    CollectionTreeNode parent = (CollectionTreeNode) node.getParent();
+		    if (parent!=null && parent.getAllowsChildren()) {
+			Gatherer.f_man.newFolder(collection_tree, parent);
+		    } else {
+			error = true;
+		    }
+		}
+	    }
+	    else {
+		error = true;
+	    }
+	    if(error) {
+		// instead of an error, we now create a new folder at the root
+		CollectionTreeNode node = (CollectionTreeNode) collection_tree.getModel().getRoot();
+		Gatherer.f_man.newFolder(collection_tree, node);
+	    }
+	}
+	else if(event.getSource() == stop_action) {
+	    file_queue.cancelAction();
+	}
+    }
+
+
+    /** Generates the pane on controls used to 'collect' files into the collection. Resposible for creating, connecting and laying out these controls. */
+    public void display()
+    {
+	// Workspace Tree
+	workspace_pane = new JPanel();
+	workspace_pane.setMinimumSize(MIN_SIZE);
+	workspace_pane.setPreferredSize(TREE_SIZE);
+	workspace_pane.setSize(TREE_SIZE);
+        workspace_pane.setComponentOrientation(Dictionary.getOrientation());
+
+	workspace_label = new JLabel(Dictionary.get("Collection.Workspace"));
+	workspace_label.setOpaque(true);
+	workspace_label.setBackground(Configuration.getColor("coloring.workspace_heading_background", false));
+	workspace_label.setForeground(Configuration.getColor("coloring.workspace_heading_foreground", false));
+	workspace_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	workspace_tree = new WorkspaceTree();
+        workspace_tree.setComponentOrientation(Dictionary.getOrientation());
+	group.add(workspace_tree);
+	workspace_scroll = new JScrollPane(workspace_tree);
+        workspace_scroll.setComponentOrientation(Dictionary.getOrientation());
+	workspace_filter = workspace_tree.getFilter();
+        workspace_filter.setComponentOrientation(Dictionary.getOrientation()); 
+        
+	card_layout = new CardLayout();
+
+	// Collection Tree
+	collection_pane = new JPanel();
+	collection_pane.setMinimumSize(MIN_SIZE);
+	collection_pane.setPreferredSize(TREE_SIZE);
+	collection_pane.setSize(TREE_SIZE);
+        collection_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	collection_label = new JLabel(Dictionary.get("Collection.Collection"));
+	collection_label.setOpaque(true);
+	collection_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	collection_tree = Gatherer.c_man.getCollectionTree();
+	collection_tree.setEnabled(Gatherer.c_man.getCollectionTreeModel() != null);
+	group.add(collection_tree);
+	collection_filter = collection_tree.getFilter();
+
+	tree_pane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
+        tree_pane.setComponentOrientation(Dictionary.getOrientation());
+	// No collection loaded pane
+	JPanel no_collection_pane = new JPanel();
+	no_collection_pane.setBackground(Color.lightGray);
+        no_collection_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel no_collection_label = new JLabel(Dictionary.get("Collection.Collection"));
+	no_collection_label.setBackground(Color.lightGray);
+	no_collection_label.setForeground(Color.black);
+	no_collection_label.setOpaque(true);
+        no_collection_label.setComponentOrientation(Dictionary.getOrientation());
+	
+	JPanel no_collection_loaded_panel = new JPanel();
+	no_collection_loaded_panel.setBorder(BorderFactory.createLineBorder(Color.black));
+	no_collection_loaded_panel.setBackground(Color.lightGray);
+        no_collection_loaded_panel.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel no_collection_loaded_label = new JLabel(Dictionary.get("Collection.No_Collection_Loaded"));
+	no_collection_loaded_label.setHorizontalAlignment(JLabel.CENTER);
+	no_collection_loaded_label.setVerticalAlignment(JLabel.CENTER);
+	no_collection_loaded_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	// Status pane
+	control_pane = new JPanel();
+        control_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel inner_pane = new JPanel();
+	inner_pane.setSize(STATUS_SIZE);
+        inner_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	card_pane = new JPanel();
+        card_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel file_pane = new JPanel();
+        file_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel progress_pane = new JPanel();
+        progress_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel file_status = file_queue.getFileStatus();
+        file_status.setComponentOrientation(Dictionary.getOrientation());
+        
+	JProgressBar progress_bar = file_queue.getProgressBar();
+        progress_bar.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	RecycleBin recycle_bin = Gatherer.recycle_bin;
+	recycle_bin.addActionListener(this);
+	recycle_bin.setMinimumSize(MIN_SIZE);
+	recycle_bin.setPreferredSize(MIN_SIZE);
+	recycle_bin.setToolTipText(Dictionary.get("Collection.Delete_Tooltip"));
+	group.add(recycle_bin);
+
+	// Layout Components.
+	workspace_pane.setLayout(new BorderLayout());
+	workspace_pane.add(workspace_label, BorderLayout.NORTH);
+	workspace_pane.add(workspace_scroll, BorderLayout.CENTER);
+	workspace_pane.add(workspace_filter, BorderLayout.SOUTH);
+	
+	collection_pane.setLayout(new BorderLayout());
+	collection_pane.add(collection_label, BorderLayout.NORTH);
+
+	no_collection_loaded_panel.setLayout(new BorderLayout());
+	no_collection_loaded_panel.add(no_collection_loaded_label, BorderLayout.CENTER);
+
+	no_collection_pane.setLayout(new BorderLayout());
+	no_collection_pane.add(no_collection_label, BorderLayout.NORTH);
+	no_collection_pane.add(no_collection_loaded_panel, BorderLayout.CENTER);
+
+	card_pane.setLayout(card_layout);
+	card_pane.add(no_collection_pane, NO_COLLECTION_LOADED_CARD);
+	card_pane.add(collection_pane, COLLECTION_LOADED_CARD);
+
+        if (Dictionary.getOrientation().isLeftToRight()){
+            tree_pane.add(workspace_pane, JSplitPane.LEFT);
+            tree_pane.add(card_pane, JSplitPane.RIGHT);
+        }else{
+            tree_pane.add(workspace_pane, JSplitPane.RIGHT);
+            tree_pane.add(card_pane, JSplitPane.LEFT);
+        }
+        
+        
+	tree_pane.setDividerLocation(TREE_SIZE.width - 10);
+
+	file_pane.setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
+	file_pane.setLayout(new BorderLayout());
+	file_pane.add(file_status, BorderLayout.CENTER);
+	file_pane.add(stop_action, BorderLayout.LINE_END);
+
+	progress_pane.setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
+	progress_pane.setLayout(new BorderLayout());
+	progress_pane.add(progress_bar, BorderLayout.CENTER);
+
+	inner_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(10,10,10,10), BorderFactory.createLoweredBevelBorder()));
+	inner_pane.setLayout(new GridLayout(2,1));
+	inner_pane.add(file_pane);
+	inner_pane.add(progress_pane);
+
+	button_pane.add(new_folder);
+	button_pane.add(recycle_bin);
+
+	control_pane.setLayout(new BorderLayout());
+	control_pane.add(inner_pane, BorderLayout.CENTER);
+	control_pane.add(button_pane, BorderLayout.LINE_END);
+
+	this.setLayout(new BorderLayout());
+	this.add(tree_pane, BorderLayout.CENTER);
+	this.add(control_pane, BorderLayout.SOUTH);
+    }
+
+
+    /** Called to inform this control panel that it has just gained focus as an effect of the user clicking on its tab.
+     */
+    public void gainFocus()
+    {
+	// Add the collection tree and filter back into this pane
+	collection_scroll = new JScrollPane(collection_tree);
+	collection_pane.add(collection_scroll, BorderLayout.CENTER);
+	collection_pane.add(collection_filter, BorderLayout.SOUTH);
+    }
+
+
+    /** Called to inform this pane that it has just lost focus as an effect of the user clicking on another tab
+     */
+    public void loseFocus()
+    {
+	// Remove the collection tree and filter from this pane so it can be added to the Enrich pane
+	collection_pane.remove(collection_scroll);
+	collection_pane.remove(collection_filter);
+    }
+
+
+    /** Retrieve a list of the currently selected file records in the collection tree. */
+    private CollectionTreeNode[] getCollectionTreeSelection()
+    {
+	TreePath paths[] = collection_tree.getSelectionPaths();
+	CollectionTreeNode records[] = null;
+	if (paths != null) {
+	    records = new CollectionTreeNode[paths.length];
+	    for (int i = 0; i < records.length; i++) {
+		records[i] = (CollectionTreeNode) paths[i].getLastPathComponent();
+	    }
+	}
+	return records;
+    }
+
+    
+    /** Called whenever the detail mode changes to ensure the filters are at an appropriate level (ie only editable by those that understand regular expression matching)
+     * @param mode the mode level as an int
+     */
+    public void modeChanged(int mode) {
+	collection_filter.setEditable(mode >= Configuration.LIBRARIAN_MODE);
+	workspace_filter.setEditable(mode >= Configuration.LIBRARIAN_MODE);
+    }
+
+
+    /** Refresh this pane, depending on what has just happened (refresh_reason). */
+    public void refresh(int refresh_reason, boolean collection_loaded)
+    {
+	if (collection_loaded) {
+	    // Update collection tree
+	    if (refresh_reason == Gatherer.COLLECTION_OPENED) {
+		collection_tree.setModel(Gatherer.c_man.getCollectionTreeModel());
+	    }
+
+	    card_layout.show(card_pane, COLLECTION_LOADED_CARD);
+	}
+	else {
+	    // Update collection tree
+	    collection_tree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode("Error")));
+
+	    card_layout.show(card_pane, NO_COLLECTION_LOADED_CARD);
+	}
+
+	// File sizes turned on/off
+	if (refresh_reason == Gatherer.PREFERENCES_CHANGED) {
+	    refreshWorkspaceTree(DragTree.TREE_DISPLAY_CHANGED);
+	    refreshCollectionTree(DragTree.TREE_DISPLAY_CHANGED);
+	}
+
+	// Enable or disable the controls
+	collection_tree.setEnabled(collection_loaded);
+	collection_filter.setEnabled(collection_loaded);
+	new_folder.setEnabled(collection_loaded);
+    }
+
+
+    public void refreshCollectionTree(int refresh_reason) {
+	collection_tree.refresh(null);
+    }
+
+
+    public void refreshWorkspaceTree(int refresh_reason) {
+	workspace_tree.refresh(refresh_reason);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/HelpFrame.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/HelpFrame.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/HelpFrame.java	(revision 31635)
@@ -0,0 +1,395 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Principal Author: John Thompson, NZDL Project, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+
+/**
+ * This class provides a nice help facility. It is a separate frame that can be positioned
+ * as the user wishes, and provides a contents page and the help information.
+ * @author Michael Dewsnip, NZDL Project
+ */
+public class HelpFrame 
+    extends JFrame
+{
+    /** The size of the frame */
+    static private final Dimension SIZE = new Dimension(800, 560);
+
+    static private HelpFrame self = null;
+
+    /** The help view at the bottom of the frame. */
+    static private JEditorPane help_pane = null;
+    /** The help contents tree at the top of the frame. */
+    static private JTree help_contents_tree = null;
+    /** The help contents tree model. */
+    static private HelpContentsTreeModel help_contents_tree_model = null;
+    /** The split between the contents tree and the page view. */
+    static private JSplitPane split_pane = null;
+
+    public HelpFrame()
+    {
+	setDefaultCloseOperation(HIDE_ON_CLOSE);
+	setSize(SIZE);
+	setTitle(Dictionary.get("Help.Title"));
+        this.setComponentOrientation(Dictionary.getOrientation());
+        
+	help_pane = new JEditorPane();
+        help_pane.setComponentOrientation(Dictionary.getOrientation());
+	help_pane.setEditable(false);
+	help_pane.addHyperlinkListener(new HelpPaneHyperlinkListener());
+
+	HelpContentsTreeNode help_tree_root_node = new HelpContentsTreeNode(null, Dictionary.get("Help.Contents"));
+	help_contents_tree_model = new HelpContentsTreeModel(help_tree_root_node);
+	help_contents_tree = new JTree((DefaultTreeModel) help_contents_tree_model);
+        help_contents_tree.setComponentOrientation(Dictionary.getOrientation());
+	help_contents_tree.addTreeSelectionListener(new HelpContentsTreeSelectionListener());
+	help_contents_tree.setExpandsSelectedPaths(true);
+
+	// Creation
+	JPanel content_pane = (JPanel) this.getContentPane();
+	content_pane.setComponentOrientation(Dictionary.getOrientation());
+        split_pane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
+	JScrollPane help_contents_tree_scroll = new JScrollPane(help_contents_tree);
+        help_contents_tree_scroll.setComponentOrientation(Dictionary.getOrientation());
+	JScrollPane help_pane_scroll = new JScrollPane(help_pane);
+        help_pane_scroll.setComponentOrientation(Dictionary.getOrientation());
+
+	// Layout
+        if (Dictionary.getOrientation().isLeftToRight()){
+            split_pane.add(help_contents_tree_scroll, JSplitPane.LEFT);
+            split_pane.add(help_pane_scroll, JSplitPane.RIGHT);
+        }else{
+            split_pane.add(help_contents_tree_scroll, JSplitPane.RIGHT);
+            split_pane.add(help_pane_scroll, JSplitPane.LEFT);
+        }
+        
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(split_pane, BorderLayout.CENTER);
+
+	// Center
+	Dimension screen_size = Toolkit.getDefaultToolkit().getScreenSize();
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+
+
+	// Pretty corner icon
+	String gsmall_image = "gatherer.png";
+	if (Configuration.fedora_info.isActive()) {
+	    gsmall_image = "fli-" + gsmall_image;
+	}
+
+	this.setIconImage(JarTools.getImage(gsmall_image).getImage());
+
+	self = this;
+    }
+
+    public void destroy()
+    {
+	help_contents_tree = null;
+	help_pane = null;
+    }
+
+
+    /** Retrieve the relative file path to the language-specific help index xml file. */
+    private String getHelpFolder()
+    {
+	String help_folder = "help/" + Configuration.getLanguage() + "/";
+
+	// Applet case: get help files out of JAR file
+	if (Gatherer.isApplet && JarTools.getResource("/" + help_folder) != null) {
+	    return help_folder;
+	}
+
+	// Normal case: get help files out of GLI "help" folder
+	if (new File(help_folder).exists()) {
+	    return help_folder;
+	}
+
+	// Resort to English
+	return "help/" + StaticStrings.ENGLISH_LANGUAGE_STR  + "/";
+    }
+
+
+    private URL getURLForSection(String section_name)
+    {
+	// Form the path to the help file from the section name
+	if (section_name != null) {
+	    String help_file_path = getHelpFolder() + section_name + StaticStrings.HTM_FILE_EXTENSION;
+
+	    try {
+		// Applet case: get help file out of JAR file
+		if (Gatherer.isApplet) {
+		    return JarTools.getResource("/" + help_file_path);
+		}
+
+		// Normal case: get help file out of GLI "help" folder
+		return (new File(help_file_path)).toURI().toURL();
+	    }
+	    catch (Exception exception) {
+		DebugStream.printStackTrace(exception);
+	    }
+	}
+
+	return null;
+    }
+
+
+    static private void selectHelpContentsTreeNode(String section_name)
+    {
+	// Find the tree node with this section name, then select it
+	selectHelpContentsTreeNode(help_contents_tree_model.getHelpContentsTreeNodeNamed(section_name));
+    }
+
+
+    static private void selectHelpContentsTreeNode(HelpContentsTreeNode help_tree_node)
+    {
+	// Ensure the node in the help contents tree is selected and visible
+	TreePath help_tree_node_path = new TreePath(help_tree_node.getPath());
+	help_contents_tree.setSelectionPath(help_tree_node_path);
+	help_contents_tree.scrollPathToVisible(help_tree_node_path);
+    }
+
+
+    static public void setView(String section_name)
+    {
+	// Select the appropriate node in the help contents tree
+	selectHelpContentsTreeNode(section_name);
+
+	// Show the help page with the specified section name
+	self.showHelpPage(section_name);
+    }
+
+
+    private void showHelpPage(String section_name)
+    {
+	// Find the tree node with this section name, then show the page for the node
+	showHelpPage(getURLForSection(section_name));
+    }
+
+
+    private void showHelpPage(URL help_page_url)
+    {
+	// Display the selected page
+	if (help_page_url != null) {
+	    try {
+		help_pane.setPage(help_page_url);
+	    }
+	    catch (Exception exception) {
+		DebugStream.printStackTrace(exception);
+	    }
+	}
+
+	// Make the help frame visible
+	setVisible(true);
+	split_pane.setDividerLocation(0.4);
+    }
+
+
+    private class HelpContentsTreeModel
+	extends DefaultTreeModel
+    {
+	public HelpContentsTreeModel(MutableTreeNode help_tree_root_node)
+	{
+	    super(help_tree_root_node);
+
+	    // Load the XML help index file and build the contents structure from it
+	    try {
+		String help_index_file_path = getHelpFolder() + "help_index.xml";
+		Document document = null;
+		if (Gatherer.isApplet && JarTools.getResource(help_index_file_path) != null) {
+		    document = XMLTools.parseXMLFile(help_index_file_path, true);
+		}
+		
+		if (new File(help_index_file_path).exists()) {
+		    document = XMLTools.parseXMLFile(help_index_file_path, false);
+		}		
+
+		if (document == null) {
+		    String errormsg = "HelpFrame.HelpContentsTreeModel constructor(): There's no help document after parsing";
+		    System.err.println(errormsg);
+		    DebugStream.println(errormsg);
+		    return;
+		}
+		// Traverse the help hierarchy, building a tree representing its structure
+		Node document_node = document.getFirstChild();
+
+		// Add a help contents tree node for each major section
+		int section_number = 0;
+		NodeList children = document_node.getChildNodes();
+		for (int i = 0; i < children.getLength(); i++) {
+		    Node child = children.item(i);
+		    if (child.getNodeName().equals(StaticStrings.SECTION_ELEMENT)) {
+			section_number++;
+			buildHelpContentsTree(help_tree_root_node, section_number + StaticStrings.EMPTY_STR, child);
+		    }
+		}
+	    }
+	    catch (Exception exception) {
+		DebugStream.printStackTrace(exception);
+	    }
+	}
+
+
+	private void buildHelpContentsTree(MutableTreeNode parent, String pos, Node node)
+	{
+	    // Determine the section name
+	    String section_name = ((Element) node).getAttribute(StaticStrings.NAME_ATTRIBUTE);
+
+	    // Determine the section title
+	    String section_title = "";
+	    NodeList children = node.getChildNodes();
+	    for (int i = 0; i < children.getLength(); i++) {
+		Node child = children.item(i);
+		if (child.getNodeName().equals(StaticStrings.TITLE_ELEMENT)) {
+		    section_title = pos + ": ";
+		    if (child.getFirstChild() != null) {
+			section_title += child.getFirstChild().getNodeValue();
+		    }
+		}
+	    }
+
+	    // Add the node into the tree
+	    HelpContentsTreeNode help_tree_node = new HelpContentsTreeNode(section_name, section_title);
+	    insertNodeInto(help_tree_node, parent, parent.getChildCount());
+
+	    // Apply recursively to the children of this node
+	    int section_number = 0;
+	    for (int i = 0; i < children.getLength(); i++) {
+		Node child = children.item(i);
+		if (child.getNodeName().equals(StaticStrings.SECTION_ELEMENT)) {
+		    section_number++;
+		    buildHelpContentsTree(help_tree_node, pos + StaticStrings.STOP_CHARACTER + section_number, child);
+		}
+	    }
+	}
+
+
+	private HelpContentsTreeNode getHelpContentsTreeNodeNamed(String section_name)
+	{
+	    // Find the node with name matching section name, somewhere in the tree
+	    if (section_name != null) {
+		Enumeration descendants = ((DefaultMutableTreeNode) root).preorderEnumeration();
+		while (descendants.hasMoreElements()) {
+		    HelpContentsTreeNode help_tree_node = (HelpContentsTreeNode) descendants.nextElement();
+		    if (section_name.equals(help_tree_node.section_name)) {
+			return help_tree_node;
+		    }
+		}
+	    }
+
+	    // Couldn't be found, so just return the root (Contents) node
+	    return (HelpContentsTreeNode) root;
+	}
+    }
+
+
+    /** This listener is used to listen for selection changes in the contents tree, and
+     * to show the appropriate page as required.
+     */
+    private class HelpContentsTreeSelectionListener
+	implements TreeSelectionListener
+    {
+	/** Any implementation of <i>TreeSelectionListener</i> must include this method so we can be informed when the tree selection changes.
+	 * @param event A <strong>TreeSelectionEvent</strong> containing al the relevant information about the event itself.
+	 */
+	public void valueChanged(TreeSelectionEvent event)
+	{
+	    HelpContentsTreeNode help_tree_node = (HelpContentsTreeNode) event.getPath().getLastPathComponent();
+	    selectHelpContentsTreeNode(help_tree_node);
+	    showHelpPage(help_tree_node.section_name);
+	}
+    }
+
+
+    /** This class provides a wrapper around a <strong>DefaultMutableTreeNode</strong> which
+     * provides the ability to set an html page to be loaded when this node is selected.
+     */
+    private class HelpContentsTreeNode 
+	extends DefaultMutableTreeNode
+    {
+	/** The unique name of the section (matches the HTML filename) */
+	public String section_name = null;
+	/** The title to be displayed for this tree node. */
+	public String section_title = null;
+
+	public HelpContentsTreeNode(String section_name, String section_title)
+	{
+	    this.section_name = section_name;
+	    this.section_title = section_title;
+	}
+
+	public String toString()
+	{
+	    return section_title;
+	}
+    }
+
+
+    private class HelpPaneHyperlinkListener
+	implements HyperlinkListener
+    {
+	public void hyperlinkUpdate(HyperlinkEvent e)
+	{
+	    if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
+		// Determine which node in the help contents tree to select, then select it
+		String section_link = e.getURL().getFile();
+		if (section_link.endsWith(".htm")) {
+		    section_link = section_link.substring(section_link.lastIndexOf("/") + 1);
+		    section_link = section_link.substring(0, section_link.length() - ".htm".length());
+		    selectHelpContentsTreeNode(section_link);
+		}
+
+		// Change to the page specified by the link
+		showHelpPage(e.getURL());
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/LockFileDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/LockFileDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/LockFileDialog.java	(revision 31635)
@@ -0,0 +1,246 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+/** A lock file dialog is shown whenever GLI detects an existing lock on a collection it is trying to open. The user can look at the details provided with the dialog, and decide whether to 'steal' the lock or to cancel the loading. */
+public class LockFileDialog
+    extends ModalDialog {
+
+    private int choice;
+    private Document document;
+    private JButton cancel_button;
+    private JButton ok_button;
+    private LockFileDialog self;
+
+    static final public int NO_OPTION  = 0;
+    static final public int YES_OPTION = 1;
+
+    static final private Dimension LABEL_SIZE = new Dimension(100, 25);
+    static final private Dimension SIZE = new Dimension(500, 250);
+    static final private int ICON_SIZE = 30;
+
+    public LockFileDialog(JFrame parent, String name, File lock_file) {
+	super(parent, Dictionary.get("LockFileDialog.Title"), true);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	setSize(SIZE);
+	setJMenuBar(new SimpleMenuBar("openingacollection"));
+	this.self = this;
+
+	// Parse the lock file
+	document = XMLTools.parseXMLFile(lock_file);
+
+	// Creation
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel upper_pane = new JPanel();
+        upper_pane.setComponentOrientation(Dictionary.getOrientation());
+	ImageIcon icon = JarTools.getImage("lcolicn.gif");
+	ImageIcon scaled_icon = new ImageIcon(icon.getImage().getScaledInstance(ICON_SIZE, ICON_SIZE, Image.SCALE_DEFAULT));
+	JLabel icon_label = new JLabel(scaled_icon);
+        icon_label.setComponentOrientation(Dictionary.getOrientation());
+	JPanel title_pane = new JPanel();
+        title_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel title_one_label = new JLabel(Dictionary.get("General.Warning"));
+	title_one_label.setComponentOrientation(Dictionary.getOrientation());
+	JTextArea title_two_textarea = new JTextArea(Dictionary.get("LockFileDialog.Lockfile_Message_One"));
+        title_two_textarea.setComponentOrientation(Dictionary.getOrientation());
+	title_two_textarea.setEditable(false);
+	title_two_textarea.setLineWrap(true);
+	title_two_textarea.setOpaque(false);
+	title_two_textarea.setRows(3);
+	title_two_textarea.setWrapStyleWord(true);
+	
+    	JPanel central_pane = new JPanel();
+        central_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel name_pane = new JPanel();
+        name_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel name_label = new JLabel(Dictionary.get("LockFileDialog.Name"));
+        name_label.setComponentOrientation(Dictionary.getOrientation());
+	name_label.setPreferredSize(LABEL_SIZE);
+	
+	JTextField name_field = new JTextField(name);
+        name_field.setComponentOrientation(Dictionary.getOrientation());
+	name_field.setEditable(false);
+	name_field.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+
+
+        JPanel person_pane = new JPanel();
+	person_pane.setComponentOrientation(Dictionary.getOrientation());
+        JLabel person_label = new JLabel(Dictionary.get("LockFileDialog.User"));
+        person_label.setComponentOrientation(Dictionary.getOrientation());
+	person_label.setPreferredSize(LABEL_SIZE);
+	
+	JTextField person_field = new JTextField(getValue("User"));
+        person_field.setComponentOrientation(Dictionary.getOrientation());
+	person_field.setEditable(false);
+	person_field.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+
+	JPanel machine_pane = new JPanel();
+        machine_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel machine_label = new JLabel(Dictionary.get("LockFileDialog.Machine"));
+        machine_label.setComponentOrientation(Dictionary.getOrientation());
+	machine_label.setPreferredSize(LABEL_SIZE);
+	
+	JTextField machine_field = new JTextField(getValue("Machine"));
+        machine_field.setComponentOrientation(Dictionary.getOrientation());
+	machine_field.setEditable(false);
+	machine_field.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+
+	JPanel created_pane = new JPanel();
+        created_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel created_label = new JLabel(Dictionary.get("LockFileDialog.Date"));
+	created_label.setPreferredSize(LABEL_SIZE);
+        created_label.setComponentOrientation(Dictionary.getOrientation());
+	
+	JTextField created_field = new JTextField(getValue("Date"));
+        created_field.setComponentOrientation(Dictionary.getOrientation());
+	created_field.setEditable(false);
+	created_field.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+
+	JLabel central_label = new JLabel(Dictionary.get("LockFileDialog.Lockfile_Message_Two"));
+	central_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("LockFileDialog.OK_Tooltip"));
+	
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("LockFileDialog.Cancel_Tooltip"));
+	
+	// Connection
+	ok_button.addActionListener(new MyActionListener());
+	cancel_button.addActionListener(new MyActionListener());
+
+	// Layout
+	icon_label.setBorder(BorderFactory.createEmptyBorder(0,0,0,10));
+
+	title_pane.setLayout(new BorderLayout());
+	title_pane.add(title_one_label, BorderLayout.NORTH);
+	title_pane.add(title_two_textarea, BorderLayout.CENTER);
+
+	upper_pane.setLayout(new BorderLayout());
+	upper_pane.add(icon_label, BorderLayout.LINE_START);
+	upper_pane.add(title_pane, BorderLayout.CENTER);
+
+	name_pane.setLayout(new BorderLayout());
+	name_pane.add(name_label, BorderLayout.LINE_START);
+	name_pane.add(name_field, BorderLayout.CENTER);
+
+	person_pane.setLayout(new BorderLayout());
+	person_pane.add(person_label, BorderLayout.LINE_START);
+	person_pane.add(person_field, BorderLayout.CENTER);
+
+	machine_pane.setLayout(new BorderLayout());
+	machine_pane.add(machine_label, BorderLayout.LINE_START);
+	machine_pane.add(machine_field, BorderLayout.CENTER);
+
+	created_pane.setLayout(new BorderLayout());
+	created_pane.add(created_label, BorderLayout.LINE_START);
+	created_pane.add(created_field, BorderLayout.CENTER);
+
+	central_pane.setLayout(new GridLayout(5,1,0,5));
+	central_pane.add(name_pane);
+	central_pane.add(person_pane);
+	central_pane.add(machine_pane);
+	central_pane.add(created_pane);
+	central_pane.add(central_label);
+
+	button_pane.setLayout(new GridLayout(1,2,5,0));
+	button_pane.add(ok_button);
+	button_pane.add(cancel_button);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(upper_pane, BorderLayout.NORTH);
+	content_pane.add(central_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	// Display
+	if (parent != null) {
+	    Rectangle frame_bounds = parent.getBounds();
+	    setLocation(frame_bounds.x + (frame_bounds.width - SIZE.width) / 2, frame_bounds.y + (frame_bounds.height - SIZE.height) / 2);
+	}
+	else {
+	    Dimension screen_size = Toolkit.getDefaultToolkit().getScreenSize();
+	    setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	}
+    }
+
+    public int getChoice() {
+	setVisible(true);
+	return choice;
+    }
+
+    private String getValue(String name) {
+	String value = "";
+	if(document != null) {
+	    try {
+		Element value_element = (Element) XMLTools.getNodeFromNamed(document.getDocumentElement(), name);
+		value = XMLTools.getValue(value_element);
+	    }
+	    catch (Exception error) {
+	    }
+	}
+	else {
+	    value = Dictionary.get("LockFileDialog.Error");
+	}
+	return value;
+    }
+
+    private class MyActionListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    if(event.getSource() == ok_button) {
+		choice = YES_OPTION;
+	    }
+	    else {
+		choice = NO_OPTION;
+	    }
+	    self.dispose();
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MenuBar.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MenuBar.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MenuBar.java	(revision 31635)
@@ -0,0 +1,311 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.Utility;
+
+/** The menu bar for the Gatherer main GUI. 
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.2
+ */
+public class MenuBar
+    extends JMenuBar
+{
+    static public ImageIcon BLANK_ICON = JarTools.getImage("blank.gif");
+    static public ImageIcon HELP_ICON = JarTools.getImage("help.png");
+
+    /** The icon to be displayed alongside the context choosen help file. */
+    private int current_tab          = -1;
+
+    public JMenu file               = null;
+    public JMenu edit               = null;
+    public JMenu help                = null;
+    public JMenuItem file_associations;
+    public JMenuItem file_cdimage    = null;
+    public JMenuItem file_close      = null;
+    public JMenuItem file_delete     = null;
+    public JMenuItem file_exit       = null;
+    public JMenuItem file_exportas   = null;
+    public JMenuItem file_new        = null;
+    public JMenuItem file_open       = null;
+    public JMenuItem file_options    = null;
+    public JMenuItem file_save       = null;
+    public JMenuItem edit_copy;
+    public JMenuItem edit_cut;
+    public JMenuItem edit_paste;
+    public JMenuItem edit_config;
+    public JMenuItem help_general;
+    public JMenuItem help_download;
+    public JMenuItem help_gather;
+    public JMenuItem help_enrich;
+    public JMenuItem help_design;
+    public JMenuItem help_create;
+    public JMenuItem help_format;
+    public JMenuItem help_about;
+
+
+    public MenuBar(MenuListener menu_listener)
+    {
+        this.setComponentOrientation(Dictionary.getOrientation());
+        
+	file = new JMenu();
+	file.setEnabled(false);
+	file.setText(Dictionary.get("Menu.File"));
+	file.setComponentOrientation(Dictionary.getOrientation());
+        
+	file_associations = new JMenuItem(Dictionary.get("Menu.File_Associations"));
+	file_associations.addActionListener(Gatherer.g_man);	  
+	file_associations.setComponentOrientation(Dictionary.getOrientation());
+        
+	file_cdimage = new JMenuItem(Dictionary.get("Menu.File_CDimage"));
+	file_cdimage.addActionListener(Gatherer.g_man);
+	file_cdimage.setEnabled(!Gatherer.isGsdlRemote);
+	file_cdimage.setComponentOrientation(Dictionary.getOrientation());
+        
+	file_close = new JMenuItem(Dictionary.get("Menu.File_Close"));
+	file_close.addActionListener(Gatherer.g_man);
+	file_close.setEnabled(false);
+	file_close.setComponentOrientation(Dictionary.getOrientation());
+        
+	file_delete = new JMenuItem(Dictionary.get("Menu.File_Delete"));
+	file_delete.addActionListener(Gatherer.g_man);
+	file_delete.setComponentOrientation(Dictionary.getOrientation());
+        
+	file_exit = new JMenuItem(Dictionary.get("Menu.File_Exit"));
+	file_exit.addActionListener(Gatherer.g_man);
+	file_exit.setComponentOrientation(Dictionary.getOrientation());
+        
+	file_exportas = new JMenuItem(Dictionary.get("Menu.File_ExportAs"));
+	file_exportas.addActionListener(Gatherer.g_man);
+	file_exportas.setEnabled(!Gatherer.isGsdlRemote);
+	file_exportas.setComponentOrientation(Dictionary.getOrientation());
+      	
+	file_new = new JMenuItem(Dictionary.get("Menu.File_New"));
+	file_new.addActionListener(Gatherer.g_man);
+	file_new.setComponentOrientation(Dictionary.getOrientation());
+        
+	file_open = new JMenuItem(Dictionary.get("Menu.File_Open"));
+	file_open.addActionListener(Gatherer.g_man);
+	file_open.setComponentOrientation(Dictionary.getOrientation());
+        
+	file_options = new JMenuItem(Dictionary.get("Menu.File_Options"));
+	file_options.addActionListener(Gatherer.g_man);
+        file_options.setComponentOrientation(Dictionary.getOrientation());
+	
+	file_save = new JMenuItem(Dictionary.get("Menu.File_Save"));
+	file_save.addActionListener(Gatherer.g_man);
+	file_save.setEnabled(false);
+	file_save.setComponentOrientation(Dictionary.getOrientation());
+        
+	// Layout (file menu)
+	file.add(file_new);
+	file.add(file_open);
+	file.add(file_save);
+	file.add(file_close);
+	file.add(new JSeparator());
+	file.add(file_delete);
+	file.add(file_exportas);
+	// these currently don't work. better to just disable them?
+	if (!Gatherer.GS3) {
+	    file.add(file_cdimage);
+	}
+	file.add(new JSeparator());
+	file.add(file_associations);
+	file.add(file_options);
+	file.add(new JSeparator());
+	file.add(file_exit);
+
+	// Edit menu
+	edit = new JMenu();
+	edit.setEnabled(false);
+	edit.setText(Dictionary.get("Menu.Edit"));
+	edit.setComponentOrientation(Dictionary.getOrientation());
+	String modkey = "ctrl";
+	if(Utility.isMac()) { // on Mac, we now use the Apple key instead of Ctrl for GLI/GEMS edit shortcuts
+	    // http://stackoverflow.com/questions/5585919/creating-unicode-character-from-its-number
+	    char appleKeyCodepoint = 0x2318; // applekey symbol unicode codepoint: U+2318 (\u2318)
+	    String appleKeySymbol = String.valueOf(appleKeyCodepoint);
+	    modkey = appleKeySymbol;
+	}
+
+	edit_cut = new JMenuItem(Dictionary.get("Menu.Edit_Cut", modkey));
+	edit_cut.addActionListener(Gatherer.g_man);
+	edit_cut.setComponentOrientation(Dictionary.getOrientation());
+        
+	edit_copy = new JMenuItem(Dictionary.get("Menu.Edit_Copy", modkey));
+	edit_copy.addActionListener(Gatherer.g_man);
+        edit_copy.setComponentOrientation(Dictionary.getOrientation());
+	
+	edit_paste = new JMenuItem(Dictionary.get("Menu.Edit_Paste", modkey));
+	edit_paste.addActionListener(Gatherer.g_man);
+	edit_paste.setComponentOrientation(Dictionary.getOrientation());
+
+	// Layout (edit menu)
+	edit.add(edit_cut);
+	edit.add(edit_copy);
+	edit.add(edit_paste);
+
+	// if GS3, then we have a menu item that allows editing of config files
+	if(Gatherer.GS3) {   
+
+	    edit_config = new JMenuItem(Dictionary.get("Menu.Edit_Config"));
+	    // handle the actual Edit > ColConfig.xml menu item
+	    edit_config.addActionListener(Gatherer.g_man);
+	    edit_config.setComponentOrientation(Dictionary.getOrientation());
+	    edit.add(edit_config);
+	    // The Edit menu itself now listens, in order to gray out the
+	    // Edit > collectionConfig.xml option when no collection is open
+	    // (JMenu doesn't work with ActionListener only with MenuListener, see
+	    // http://stackoverflow.com/questions/9862165/jmenu-actionlistener)
+	    edit.addMenuListener(menu_listener);
+
+	}
+
+	// Help menu
+	help = new JMenu();
+	help.setIcon(HELP_ICON);
+	help.setText(Dictionary.get("Menu.Help"));
+	help.setComponentOrientation(Dictionary.getOrientation());
+        
+	help_general = new JMenuItem(Dictionary.get("Source.General"));
+	help_general.addActionListener(Gatherer.g_man);
+	help_general.setComponentOrientation(Dictionary.getOrientation());
+        
+	help_download = new JMenuItem(Dictionary.get("GUI.Download"), BLANK_ICON);
+	help_download.addActionListener(Gatherer.g_man);
+	help_download.setComponentOrientation(Dictionary.getOrientation());
+        
+	help_gather = new JMenuItem(Dictionary.get("GUI.Gather"), BLANK_ICON);
+	help_gather.addActionListener(Gatherer.g_man);
+	help_gather.setComponentOrientation(Dictionary.getOrientation());
+        
+	help_enrich = new JMenuItem(Dictionary.get("GUI.Enrich"), BLANK_ICON);
+	help_enrich.addActionListener(Gatherer.g_man);
+	help_enrich.setComponentOrientation(Dictionary.getOrientation());
+        
+	help_design = new JMenuItem(Dictionary.get("GUI.Design"), BLANK_ICON);
+	help_design.addActionListener(Gatherer.g_man);
+	help_design.setComponentOrientation(Dictionary.getOrientation());
+        
+	help_create = new JMenuItem(Dictionary.get("GUI.Create"), BLANK_ICON);
+	help_create.addActionListener(Gatherer.g_man);
+	help_create.setComponentOrientation(Dictionary.getOrientation());
+        
+	help_format = new JMenuItem(Dictionary.get("GUI.Format"), BLANK_ICON);
+	help_format.addActionListener(Gatherer.g_man);
+	help_format.setComponentOrientation(Dictionary.getOrientation());
+        
+	help_about = new JMenuItem(Dictionary.get("Menu.Help_About"));
+	help_about.addActionListener(Gatherer.g_man);
+	help_about.setComponentOrientation(Dictionary.getOrientation());
+        
+	// Layout (help menu)
+	help.add(help_general);
+	help.add(new JSeparator());
+	if (Configuration.get("workflow.download", true) && Gatherer.isDownloadEnabled) {
+	    help.add(help_download);
+	}
+	if (Configuration.get("workflow.gather", true)) {
+	    help.add(help_gather);
+	}
+	if (Configuration.get("workflow.enrich", true)) {
+	    help.add(help_enrich);
+	}
+	if (Configuration.get("workflow.design", true)) {
+	    help.add(help_design);
+	}
+	if (Configuration.get("workflow.create", true)) {
+	    help.add(help_create);
+	}
+	if (Configuration.get("workflow.format", true)) {
+	    help.add(help_format);
+	}
+	help.add(new JSeparator());
+	help.add(help_about);
+
+	// Layout (menu bar)
+	this.add(file);
+	this.add(Box.createHorizontalStrut(15));
+	this.add(edit);
+	this.add(Box.createHorizontalGlue());
+	this.add(help);
+    }
+
+    /** Once a quit has been requested by the user, prevent any further menu selections. */
+    public void exit() {
+	file.setEnabled(false);
+	edit.setEnabled(false);
+	help.setEnabled(false);
+    }
+
+    public void refresh(int refresh_reason, boolean ready)
+    {
+	file_close.setEnabled(ready);
+	file_save.setEnabled(ready);
+    }
+
+
+    /** In order to provide context aware help advice we keep track of which
+     *  tab the user has open, and then highlight that help menu item with
+     *  separators.
+     *  @param tab_index The index of the selected tab (0-7).
+     */
+    public void tabSelected(int tab_index) {
+	JMenuItem selected;
+	if(current_tab != -1) {
+	    // Remove the image
+	    selected = help.getItem(current_tab);
+	    if(selected != null) {
+		selected.setIcon(BLANK_ICON);
+	    }
+	}
+	current_tab = tab_index + 2;
+	selected = help.getItem(current_tab);
+	if(selected != null) {
+	    selected.setIcon(HELP_ICON);
+	}
+	selected = null;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataElementListCellRenderer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataElementListCellRenderer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataElementListCellRenderer.java	(revision 31635)
@@ -0,0 +1,82 @@
+package org.greenstone.gatherer.gui;
+
+
+import java.awt.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataTools;
+
+
+public class MetadataElementListCellRenderer
+    extends JLabel
+    implements ListCellRenderer
+{
+    private byte previous_directionality = Character.DIRECTIONALITY_UNDEFINED;
+
+
+    public byte getDirectionality()
+    {
+	if (previous_directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT || previous_directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC || previous_directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING || previous_directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE) {
+	    return Character.DIRECTIONALITY_RIGHT_TO_LEFT;
+	}
+	else {
+	    return Character.DIRECTIONALITY_LEFT_TO_RIGHT;
+	}
+    }
+
+
+    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus)
+    {
+	if (value instanceof MetadataElement) {
+	    MetadataElement metadata_element = (MetadataElement) value;
+
+	    String interface_language_code = Configuration.getLanguage();
+	    String metadata_element_display = "";
+
+	    String metadata_element_display_name = metadata_element.getDisplayName();
+	    if (metadata_element_display_name != null) {
+		metadata_element_display += metadata_element_display_name + ":";
+	    }
+	    else {
+		metadata_element_display += metadata_element.getName() + ":";
+	    }
+
+	    String metadata_element_definition = MetadataTools.getMetadataElementAttribute(metadata_element, "definition", interface_language_code, "en");
+	    if (metadata_element_definition != null) {
+		metadata_element_display += " " + metadata_element_definition;
+	    }
+
+// 	    String metadata_element_comment = MetadataTools.getMetadataElementAttribute(metadata_element, "comment", interface_language_code, "en");
+// 	    if (metadata_element_comment != null) {
+// 		metadata_element_display += " " + metadata_element_comment;
+// 	    }
+
+	    setText(metadata_element_display);
+
+// 	    // Determine if the text should be left aligned or right aligned
+// 	    String text = metadata_element_display;
+// 	    int text_index = 0;
+// 	    byte directionality = Character.DIRECTIONALITY_UNDEFINED;
+// 	    while (directionality == Character.DIRECTIONALITY_UNDEFINED && text_index < text.length()) {
+// 		directionality = Character.getDirectionality(text.charAt(text_index));
+// 		text_index++;
+// 	    }
+// 	    if (directionality != previous_directionality) {
+// 		previous_directionality = directionality;
+// 		if (directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE) {
+// 		    setHorizontalAlignment(JLabel.TRAILING);
+// 		    setHorizontalTextPosition(JLabel.TRAILING);
+// 		    setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
+// 		}
+// 		else {
+// 		    setHorizontalAlignment(JLabel.LEADING);
+// 		    setHorizontalTextPosition(JLabel.LEADING);
+// 		    setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);
+// 		}
+// 	    }
+	}
+
+	return this;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataImportMappingPrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataImportMappingPrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataImportMappingPrompt.java	(revision 31635)
@@ -0,0 +1,197 @@
+package org.greenstone.gatherer.gui;
+
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataSet;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.metadata.MetadataTools;
+
+
+public class MetadataImportMappingPrompt
+    implements ActionListener
+{
+    final static public int ADD_BUTTON_PRESSED = 0;
+    final static public int MERGE_BUTTON_PRESSED = 1;
+    final static public int IGNORE_BUTTON_PRESSED = 2;
+    final static private Dimension DIALOG_SIZE = new Dimension(640, 180);
+
+    private int result;
+    private String metadata_element_name_full = null;
+    private GComboBox metadata_sets_combobox = null;
+    private GComboBox metadata_elements_combobox = null;
+    private JButton add_button = null;
+    private JButton merge_button = null;
+    private JButton ignore_button = null;
+    private JDialog on_screen = null;
+
+
+    public MetadataImportMappingPrompt(String metadata_element_name_full)
+    {
+	this.metadata_element_name_full = metadata_element_name_full;
+
+	// Construction and configuration
+	JDialog dialog = new ModalDialog(Gatherer.g_man);
+        dialog.setComponentOrientation(Dictionary.getOrientation());
+	dialog.setModal(true);
+	dialog.setSize(DIALOG_SIZE);
+	dialog.setJMenuBar(new SimpleMenuBar("importingpreviouslyassignedmetadata"));
+	dialog.setTitle(Dictionary.get("MIMP.Title"));
+	
+	// All the loaded metadata sets except the extracted metadata set are applicable
+	ArrayList metadata_sets = MetadataSetManager.getMetadataSets();
+	for (int i = metadata_sets.size() - 1; i >= 0; i--) {
+	    if (((MetadataSet) metadata_sets.get(i)).getNamespace().equals(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE)) {
+		metadata_sets.remove(i);
+	    }
+	}
+
+  	add_button = new GLIButton(Dictionary.get("MIMP.Add"), Dictionary.get("MIMP.Add_Tooltip"));
+ 	add_button.addActionListener(this);
+  	
+  	merge_button = new GLIButton(Dictionary.get("MIMP.Merge"), Dictionary.get("MIMP.Merge_Tooltip"));
+  	merge_button.addActionListener(this);
+  	merge_button.setEnabled(true);
+  	
+  	ignore_button = new GLIButton(Dictionary.get("MIMP.Ignore"), Dictionary.get("MIMP.Ignore_Tooltip"));
+  	ignore_button.addActionListener(this);
+  	ignore_button.setEnabled(true);
+  	
+	// !! Need to add instructions: "MIMP.Instructions", args: source elem
+	metadata_elements_combobox = new GComboBox();
+
+	metadata_sets_combobox = new GComboBox(metadata_sets);
+	metadata_sets_combobox.addActionListener(new MetadataSetListSelectionListener());
+	metadata_sets_combobox.setSelectedIndex(0);
+
+	// Layout
+	JPanel left_pane = new JPanel();
+        left_pane.setComponentOrientation(Dictionary.getOrientation());
+	left_pane.setLayout(new GridLayout(3,1));
+        
+        JLabel tmp_lable;
+        
+        tmp_lable = new JLabel(Dictionary.get("MIMP.Source_Element"));
+        tmp_lable.setComponentOrientation(Dictionary.getOrientation());
+	left_pane.add(tmp_lable);
+        
+        tmp_lable = new JLabel(Dictionary.get("MIMP.Target_Set"));
+        tmp_lable.setComponentOrientation(Dictionary.getOrientation());
+	left_pane.add(tmp_lable);
+        
+        tmp_lable = new JLabel(Dictionary.get("MIMP.Target_Element"));
+        tmp_lable.setComponentOrientation(Dictionary.getOrientation());
+	left_pane.add(tmp_lable);
+
+	JPanel right_pane = new JPanel();
+        right_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	right_pane.setLayout(new GridLayout(3,1));
+	tmp_lable = new JLabel(metadata_element_name_full);
+        tmp_lable.setComponentOrientation(Dictionary.getOrientation());
+        right_pane.add(tmp_lable);
+	right_pane.add(metadata_sets_combobox);
+	right_pane.add(metadata_elements_combobox);
+
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	button_pane.setBorder(BorderFactory.createEmptyBorder(0,5,5,5));
+	button_pane.setLayout(new GridLayout(1,3));
+	button_pane.add(add_button);
+	button_pane.add(merge_button);
+	button_pane.add(ignore_button);
+
+	JPanel content_pane = (JPanel) dialog.getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setLayout(new BorderLayout());
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.add(left_pane, BorderLayout.LINE_START);
+	content_pane.add(right_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	// Display
+	Dimension screen_size = Configuration.screen_size;
+	dialog.setLocation((screen_size.width - DIALOG_SIZE.width) / 2, (screen_size.height - DIALOG_SIZE.height) / 2);
+	on_screen = dialog;
+	on_screen.setVisible(true); // Blocks until hidden.
+	on_screen.dispose();
+	on_screen = null;
+    }
+
+
+    /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured.
+     * @param event An <strong>ActionEvent</strong> containing information gatherer when this event occured.
+     */
+    public void actionPerformed(ActionEvent event)
+    {
+	Object esrc = event.getSource();
+
+	if (esrc == add_button) {
+	    result = ADD_BUTTON_PRESSED;
+	}
+	if (esrc == merge_button) {
+	    result = MERGE_BUTTON_PRESSED;
+	}
+	if (esrc == ignore_button) {
+	    result = IGNORE_BUTTON_PRESSED;
+	}
+
+	on_screen.setVisible(false);
+    }
+
+
+    public MetadataElement getSelectedMetadataElement()
+    {
+	return (MetadataElement) metadata_elements_combobox.getSelectedItem();
+    }
+
+
+    public MetadataSet getSelectedMetadataSet()
+    {
+	return (MetadataSet) metadata_sets_combobox.getSelectedItem();
+    }
+
+
+    public int getResult()
+    {
+	return result;
+    }
+
+
+    private class MetadataSetListSelectionListener
+	implements ActionListener
+    {
+	public void actionPerformed(ActionEvent event)
+	{
+	    boolean enable_add_button = true;
+	    String metadata_element_name = MetadataTools.getMetadataElementName(metadata_element_name_full);
+
+	    MetadataSet metadata_set = (MetadataSet) metadata_sets_combobox.getSelectedItem();
+	    ArrayList metadata_elements = metadata_set.getMetadataSetElements();
+
+	    metadata_elements_combobox.removeAllItems();
+	    for (int i = 0; i < metadata_elements.size(); i++) {
+		MetadataElement metadata_element = (MetadataElement) metadata_elements.get(i);
+		metadata_elements_combobox.addItem(metadata_element);
+		if (metadata_element.getName().equals(metadata_element_name)) {
+		    metadata_elements_combobox.setSelectedIndex(i);
+		    enable_add_button = false;
+		}
+	    }
+
+	    add_button.setEnabled(enable_add_button);
+
+	    // are there any elements in the set? if not (e.g. with exp set),
+	    // disable merge
+	    if (metadata_elements.size()==0) {
+		merge_button.setEnabled(false);
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataSetDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataSetDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataSetDialog.java	(revision 31635)
@@ -0,0 +1,428 @@
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import java.util.ArrayList;
+import java.util.Vector;
+import java.io.File;
+import java.util.Observer;
+import java.util.Observable;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.DynamicListModel;
+import org.greenstone.gatherer.metadata.MetadataSet;
+import org.greenstone.gatherer.metadata.MetadataSetManager;
+import org.greenstone.gatherer.gems.*;
+
+public class MetadataSetDialog 
+    extends ModalDialog {
+
+    static private Dimension SIZE = new Dimension(600, 300);
+    static private Dimension ADD_SIZE = new Dimension(600, 500);
+    
+    private ArrayList current_metadata_sets;
+    private DynamicListModel current_metadata_model;
+
+    private JButton add_button = null;
+    private JButton edit_button = null;
+    private JButton remove_button = null;
+
+    private JButton close_button = null;
+    
+    private JList current_set_list = null;
+    private MetadataSetDialog set_dialog = null;
+    private boolean sets_changed = false;
+
+    private GEMS gems = null;
+    
+    public MetadataSetDialog() {
+	super(Gatherer.g_man, true);
+	set_dialog = this;
+	this.setComponentOrientation(Dictionary.getOrientation());
+	setJMenuBar(new SimpleMenuBar("selectingmetadatasets"));
+	setSize(SIZE);
+	setTitle(Dictionary.get("MetadataSetDialog.Title"));
+	
+	current_metadata_sets = MetadataSetManager.getMetadataSets();
+	current_metadata_model = new DynamicListModel();
+	
+	int current_size = current_metadata_sets.size();
+	for (int i=0; i<current_size; i++) {
+	    current_metadata_model.addElement(current_metadata_sets.get(i));
+	}
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+        content_pane.setOpaque(true);
+	
+	JLabel current_metadata_sets_label = new JLabel(Dictionary.get("MetadataSetDialog.Current_Sets"));
+	current_metadata_sets_label.setComponentOrientation(Dictionary.getOrientation());
+        current_metadata_sets_label.setOpaque(true);
+	current_set_list = new JList(current_metadata_model);
+        current_set_list.setComponentOrientation(Dictionary.getOrientation());
+	current_set_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	add_button = new GLIButton(Dictionary.get("MetadataSetDialog.Add"), Dictionary.get("MetadataSetDialog.Add_Tooltip"));
+	add_button.setEnabled(true);
+	
+	edit_button = new GLIButton(Dictionary.get("MetadataSetDialog.Edit"), Dictionary.get("MetadataSetDialog.Edit_Tooltip"));
+	edit_button.setEnabled(false);
+	
+	remove_button = new GLIButton(Dictionary.get("MetadataSetDialog.Remove"), Dictionary.get("MetadataSetDialog.Remove_Tooltip"));
+	remove_button.setEnabled(false);
+	
+	close_button = new GLIButton(Dictionary.get("General.Close"), Dictionary.get("General.Close_Tooltip"));
+	close_button.setEnabled(true);
+
+
+	// Add listeners
+	add_button.addActionListener(new AddButtonListener());
+	edit_button.addActionListener(new EditButtonListener());
+	remove_button.addActionListener(new RemoveButtonListener());
+	close_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    set_dialog.dispose();
+		}
+	    });
+	current_set_list.addListSelectionListener(new MetadataSetListSelectionListener());
+
+	button_pane.setLayout(new GridLayout(2,3));
+	button_pane.add(add_button);
+	button_pane.add(edit_button);
+	button_pane.add(remove_button);
+	button_pane.add(new JPanel());
+	button_pane.add(new JPanel());
+	button_pane.add(close_button);
+
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(current_metadata_sets_label, BorderLayout.NORTH);
+        JScrollPane scrol_tmp;
+        scrol_tmp = new JScrollPane(current_set_list);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.add(scrol_tmp, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+	
+	// Show
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(true);
+
+    }
+
+    public boolean setsChanged() {
+	return sets_changed;
+    }
+    
+    private class AddButtonListener
+	implements ActionListener {
+
+	public void actionPerformed(ActionEvent event) {
+	    AddMetadataSetPrompt amsp = new AddMetadataSetPrompt();
+	    if (!amsp.isCancelled()) {
+		sets_changed = true;
+	    }
+	}
+    }
+    
+    private class EditButtonListener
+	implements ActionListener, GEMSListener {
+	
+	public void actionPerformed(ActionEvent event) {
+	    // do a pop up message
+	    MetadataSet metadata_set = (MetadataSet) current_set_list.getSelectedValue();
+	    String metadata_path = metadata_set.getMetadataSetFile().toString();
+	    if (gems == null){
+		gems = new GEMS(Configuration.gsdl_path,Configuration.gsdl3_path,"", false, false);
+		gems.addGEMSListener(this);
+	    }
+	    gems.displayMetadataSet(metadata_path);
+	}
+
+	public void gemsIsClosed()
+	{
+	    // We assume that the selected metadata was edited, so reload it and remember to update the table
+	    MetadataSet edited_metadata_set = (MetadataSet) current_set_list.getSelectedValue();
+	    MetadataSetManager.unloadMetadataSet(edited_metadata_set);
+	    MetadataSetManager.loadMetadataSet(edited_metadata_set.getMetadataSetFile());
+	    sets_changed = true;
+	}
+    }
+
+    private class RemoveButtonListener
+	implements ActionListener {
+	
+	public void actionPerformed(ActionEvent event) {
+	    MetadataSet metadata_set = (MetadataSet) current_set_list.getSelectedValue();
+	    current_metadata_model.removeElement(metadata_set);
+	    Gatherer.c_man.removeMetadataSet(metadata_set);
+	    sets_changed = true;
+	    
+	}
+
+    }
+
+
+    private class MetadataSetListSelectionListener
+	implements ListSelectionListener {
+	
+	public void valueChanged(ListSelectionEvent event)
+	{
+	    // Wait until we get a stable event
+	    if (event.getValueIsAdjusting()) {
+		return;
+	    }
+
+	    // Now we can process it
+	    if (!current_set_list.isSelectionEmpty() && !((MetadataSet)current_set_list.getSelectedValue()).getNamespace().equals(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE)) {
+		remove_button.setEnabled(true);
+		edit_button.setEnabled(true);
+	    }
+	    else { 
+		remove_button.setEnabled(false);
+		edit_button.setEnabled(false);
+	    }
+	    
+	}
+	
+    }
+
+    private class AddMetadataSetPrompt
+	extends ModalDialog implements GEMSListener{
+	
+	private JDialog add_set_dialog;
+	private boolean cancelled = false;
+	
+	private JList elements_list;
+	private JList available_sets_list;
+
+	private JButton add_button;
+
+	private JButton new_button;
+     
+	private  GEMSListener self;
+
+	public AddMetadataSetPrompt() {
+	    super(Gatherer.g_man, true);
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    add_set_dialog = this;
+	    setModal(true);
+	    setJMenuBar(new SimpleMenuBar("choosingmetadatasets"));
+	    setSize(ADD_SIZE);
+	    setTitle(Dictionary.get("MetadataSetDialog.Add_Title"));
+	    self = this;
+	   
+
+	    JPanel center_pane = new JPanel();
+            center_pane.setComponentOrientation(Dictionary.getOrientation());
+	    JPanel sets_pane = new JPanel();
+            sets_pane.setComponentOrientation(Dictionary.getOrientation());
+	    JLabel sets_label = new JLabel(Dictionary.get("MetadataSetDialog.Available_Sets"));
+            sets_label.setComponentOrientation(Dictionary.getOrientation());
+	    sets_label.setOpaque(false);
+	    
+	    available_sets_list = new JList(getValidSetModel());
+            available_sets_list.setComponentOrientation(Dictionary.getOrientation());
+	    available_sets_list.addListSelectionListener(new AvailableSetListSelectionListener());
+	    available_sets_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+            
+	    JPanel elements_pane = new JPanel();
+            elements_pane.setComponentOrientation(Dictionary.getOrientation());
+	    JLabel elements_label = new JLabel(Dictionary.get("MetadataSetDialog.Elements"));
+	    elements_label.setComponentOrientation(Dictionary.getOrientation());
+            
+	    elements_list = new JList();
+            elements_list.setComponentOrientation(Dictionary.getOrientation());
+	    elements_list.setCellRenderer(new MetadataElementListCellRenderer());
+	    elements_list.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    elements_list.setForeground(Configuration.getColor("coloring.collection_tree_foreground", false));
+	    elements_list.setSelectionBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    elements_list.setSelectionForeground(Configuration.getColor("coloring.collection_tree_foreground", false));
+	    
+	    JPanel button_pane = new JPanel();
+            button_pane.setComponentOrientation(Dictionary.getOrientation());
+	    add_button = new GLIButton(Dictionary.get("MetadataSetDialog.Add_Set"), Dictionary.get("MetadataSetDialog.Add_Set_Tooltip"));
+	    add_button.setEnabled(false);
+          
+	    new_button = new GLIButton(Dictionary.get("MetadataSetDialog.New_Set"), Dictionary.get("MetadataSetDialog.New_Set_Tooltip"));
+	   
+	    JButton browse_button = new GLIButton(Dictionary.get("MetadataSetDialog.Browse"), Dictionary.get("MetadataSetDialog.Browse_Tooltip"));
+	    browse_button.setEnabled(true);
+	    JButton cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Pure_Cancel_Tooltip"));
+	    
+	    add_button.addActionListener(new AddSetActionListener());
+	    browse_button.addActionListener(new BrowseActionListener());
+	    cancel_button.addActionListener(new ActionListener() {
+		    public void actionPerformed(ActionEvent event) {
+			cancelled = true;
+			if (gems != null) {
+			    gems.removeGEMSListener(self);
+			}
+			add_set_dialog.dispose();
+		    }
+		});
+	    
+	    new_button.addActionListener(new ActionListener() {
+		    public void actionPerformed(ActionEvent event) {
+			if (gems == null) {
+			    gems = new GEMS(Configuration.gsdl_path, Configuration.gsdl3_path, "", false, false);
+			}
+			gems.newMetadataSet();
+			gems.addGEMSListener(self);
+		    }
+		});
+            	    	    
+
+	    // Layout
+	    sets_pane.setLayout(new BorderLayout());
+	    sets_pane.add(sets_label, BorderLayout.NORTH);
+	    sets_pane.add(new JScrollPane(available_sets_list), BorderLayout.CENTER);
+
+	    elements_pane.setLayout(new BorderLayout());
+	    elements_pane.add(elements_label, BorderLayout.NORTH);
+	    elements_pane.add(new JScrollPane(elements_list), BorderLayout.CENTER);
+
+	    center_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    center_pane.setLayout(new GridLayout(2,1,0,5));
+	    center_pane.add(sets_pane);
+	    center_pane.add(elements_pane);
+	    
+	    button_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	    button_pane.setLayout(new GridLayout(1,4,5,0));
+	    button_pane.add(add_button);
+	    button_pane.add(new_button); 
+	    button_pane.add(browse_button);
+	    button_pane.add(cancel_button);
+	    
+	    JPanel content_pane = (JPanel) getContentPane();
+            content_pane.setComponentOrientation(Dictionary.getOrientation());
+	    content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    content_pane.setLayout(new BorderLayout());
+	    content_pane.add(center_pane, BorderLayout.CENTER);
+	    content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	    // Show
+	    Dimension screen_size = Configuration.screen_size;
+	    setLocation((screen_size.width - ADD_SIZE.width) / 2, (screen_size.height - ADD_SIZE.height) / 2);
+	    setVisible(true);
+	    	   
+	}
+	
+	public DynamicListModel getValidSetModel(){
+	    // Show the metadata sets (except extracted, exploded, and currently assigned sets) in the GLI metadata folder
+	    ArrayList all_metadata_sets = MetadataSetManager.listMetadataSets(new File(Gatherer.getGLIMetadataDirectoryPath()));
+	    ArrayList current_metadata_sets = MetadataSetManager.getMetadataSets();
+	    DynamicListModel valid_sets_model = new DynamicListModel();
+	    for (int i=0; i<all_metadata_sets.size(); i++) {
+		 MetadataSet set = (MetadataSet) all_metadata_sets.get(i);
+		 if (set.getNamespace().equals(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE) || set.getNamespace().equals(MetadataSetManager.EXPLODED_METADATA_NAMESPACE)) {
+		    continue;
+		 }
+		// indexOf uses the equals() method, which for MetadataSets compares the toString() output, not the objects
+		 if (current_metadata_sets.indexOf(set)!=-1) {
+		    continue;
+		 }
+		 valid_sets_model.addElement(set);
+	     }
+	     
+	    return valid_sets_model;
+	}
+
+	
+	public void  gemsIsClosed(){ 
+	    available_sets_list.setModel(getValidSetModel()); 
+	}
+
+	public boolean isCancelled() {
+	    return cancelled;
+	}
+
+	private class AddSetActionListener
+	    implements ActionListener {
+
+	    public void actionPerformed(ActionEvent event) {
+		if (available_sets_list.isSelectionEmpty()) {
+		    return;
+		}
+		MetadataSet metadata_set = (MetadataSet) available_sets_list.getSelectedValue();
+		String namespace = metadata_set.getNamespace();
+		// have we got a variant already in the collection??
+		MetadataSet existing_set = null;
+		if ((existing_set = MetadataSetManager.getMetadataSet(namespace)) != null) {
+		    // warn that we are replacing
+		    String [] args = new String [] {metadata_set.toString(), existing_set.toString()};
+		    WarningDialog namespace_clash_dialog = new WarningDialog("warning.MetadataSetNamespaceClash", Dictionary.get("MetadataSetNamespaceClash.Title"), Dictionary.get("MetadataSetNamespaceClash.Message", args), null, true);
+		    if (namespace_clash_dialog.display()==JOptionPane.CANCEL_OPTION) {
+			namespace_clash_dialog.dispose();
+			return;
+		    }
+		    // if we have got here, then we remove the old set
+		    current_metadata_model.removeElement(existing_set);
+		    Gatherer.c_man.removeMetadataSet(existing_set);
+		    sets_changed = true;
+		    namespace_clash_dialog.dispose();
+		}
+		
+		
+		Gatherer.c_man.importMetadataSet(metadata_set);
+		metadata_set = MetadataSetManager.getMetadataSet(namespace);
+		current_metadata_model.addElement(metadata_set);
+		sets_changed = true;
+		cancelled = false;
+		add_set_dialog.dispose();
+
+	    }
+	}
+
+	private class BrowseActionListener
+	    implements ActionListener {
+
+	    public void actionPerformed(ActionEvent event) {
+		JFileChooser chooser = new JFileChooser(new File(Gatherer.getGLIMetadataDirectoryPath()));
+                chooser.setComponentOrientation(Dictionary.getOrientation());
+		chooser.setFileFilter(new MetadataSet.MetadataSetFileFilter());
+		chooser.setDialogTitle(Dictionary.get("MetadataSetDialog.Add_Title"));
+		int return_val = chooser.showDialog(Gatherer.g_man, Dictionary.get("MetadataSetDialog.Add_Set"));
+		if (return_val == JFileChooser.APPROVE_OPTION) {
+		    MetadataSet meta_set = new MetadataSet(chooser.getSelectedFile());
+		    Gatherer.c_man.importMetadataSet(meta_set);
+		    current_metadata_model.addElement(meta_set);
+		    cancelled = false;
+		    add_set_dialog.dispose();
+		} else {
+		    // we do nothing - user may want to add from the other dialog
+		}
+	    }
+	}
+	
+		
+	private class AvailableSetListSelectionListener 
+	    implements ListSelectionListener
+	{
+	    public void valueChanged(ListSelectionEvent event)
+	    {
+		// Wait until we get a stable event
+		if (event.getValueIsAdjusting()) {
+		    return;
+		}
+
+		if (!available_sets_list.isSelectionEmpty()) {
+		    // Retrieve the selected set
+		    MetadataSet metadata_set = (MetadataSet) available_sets_list.getSelectedValue();
+		    elements_list.setListData(new Vector(metadata_set.getMetadataSetElements()));
+		    add_button.setEnabled(true);
+		}
+		else {
+		    elements_list.setListData(new String[0]);
+		    add_button.setEnabled(false);
+		}
+	    }
+	}
+	
+	
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataValueTablePane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataValueTablePane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataValueTablePane.java	(revision 31635)
@@ -0,0 +1,428 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.gui;
+
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.table.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.metadata.MetadataChangedListener;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataTools;
+import org.greenstone.gatherer.metadata.MetadataValue;
+import org.greenstone.gatherer.metadata.MetadataValueTableEntry;
+import org.greenstone.gatherer.metadata.MetadataValueTableModel;
+import org.greenstone.gatherer.metadata.MetadataXMLFileManager;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.Utility;
+
+
+public class MetadataValueTablePane
+    extends JPanel
+{
+    private MetadataValueTable metadata_value_table = null;
+    /** Used to display either the MetadataValueTable (when files are selected) or a placeholder panel */
+    private CardLayout card_layout = null;
+    /** The name of the panel containing the metadata value table */
+    private String METADATA_VALUE_TABLE_CARD = "";
+    /** The name of the panel containing the "no metadata available" placeholder */
+    private String NO_METADATA_AVAILABLE_CARD = "No metadata available";
+    /** The name of the panel containing the "no files selected" placeholder */
+    private String NO_FILES_SELECTED_CARD = "No files selected";
+
+
+    public MetadataValueTablePane()
+    {
+	super();
+        this.setComponentOrientation(Dictionary.getOrientation());
+	metadata_value_table = new MetadataValueTable();
+
+	JScrollPane metadata_value_table_scroll = new JScrollPane(metadata_value_table);
+	metadata_value_table_scroll.getViewport().setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	metadata_value_table_scroll.setOpaque(true);
+        metadata_value_table_scroll.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel metadata_value_table_pane = new JPanel();
+	metadata_value_table_pane.setLayout(new BorderLayout());
+	metadata_value_table_pane.add(metadata_value_table_scroll, BorderLayout.CENTER);
+        metadata_value_table_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel no_metadata_available_label = new JLabel(Dictionary.get("EnrichPane.No_Metadata"));
+	no_metadata_available_label.setHorizontalAlignment(JLabel.CENTER);
+	no_metadata_available_label.setOpaque(false);
+	no_metadata_available_label.setVerticalAlignment(JLabel.CENTER);
+	no_metadata_available_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel no_metadata_available_pane = new JPanel();
+	no_metadata_available_pane.setLayout(new BorderLayout());
+	no_metadata_available_pane.add(no_metadata_available_label, BorderLayout.CENTER);
+        no_metadata_available_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel no_files_selected_label = new JLabel(Dictionary.get("EnrichPane.No_File"));
+	no_files_selected_label.setHorizontalAlignment(JLabel.CENTER);
+	no_files_selected_label.setOpaque(false);
+	no_files_selected_label.setVerticalAlignment(JLabel.CENTER);
+	no_files_selected_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel no_files_selected_pane = new JPanel();
+	no_files_selected_pane.setLayout(new BorderLayout());
+	no_files_selected_pane.add(no_files_selected_label, BorderLayout.CENTER);
+        no_files_selected_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	card_layout = new CardLayout();
+
+	this.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));
+	this.setFont(Configuration.getFont("general.font", false));
+	this.setLayout(card_layout);
+	this.add(metadata_value_table_pane, METADATA_VALUE_TABLE_CARD);
+	this.add(no_metadata_available_pane, NO_METADATA_AVAILABLE_CARD);
+	this.add(no_files_selected_pane, NO_FILES_SELECTED_CARD);
+    }
+
+
+    public void addMetadataValueTableListSelectionListener(ListSelectionListener list_selection_listener)
+    {
+	metadata_value_table.getSelectionModel().addListSelectionListener(list_selection_listener);
+    }
+
+
+    public void addMetadataValueTableMouseListener(MouseListener mouse_listener)
+    {
+	metadata_value_table.addMouseListener(mouse_listener);
+    }
+
+
+    public void addMetadataValueTextFieldDocumentListener(DocumentListener document_listener)
+    {
+	metadata_value_table.getMetadataValueTextField().getDocument().addDocumentListener(document_listener);
+    }
+
+
+    public void addMetadataValueTextFieldKeyListener(KeyListener key_listener)
+    {
+	metadata_value_table.getMetadataValueTextField().addKeyListener(key_listener);
+    }
+
+
+    public MetadataValueTableEntry getSelectedMetadataValueTableEntry()
+    {
+	return metadata_value_table.getSelectedMetadataValueTableEntry();
+    }
+
+
+    public boolean isMouseEventForInheritedMetadataValueTableColumn(MouseEvent mouse_event)
+    {
+	return metadata_value_table.isMouseEventForInheritedMetadataValueTableColumn(mouse_event);
+    }
+
+
+    public void setMetadataValueTextFieldValue(String metadata_value_string)
+    {
+	metadata_value_table.setMetadataValueTextFieldValue(metadata_value_string);
+    }
+
+
+    public void stopEditingAndAddBlankRowForSelectedMetadataElement()
+    {
+	metadata_value_table.stopEditing();
+	metadata_value_table.addBlankRowForSelectedMetadataElement();
+    }
+
+
+    public void stopEditingAndRebuild(CollectionTreeNode[] file_nodes)
+    {
+	metadata_value_table.stopEditing();
+	metadata_value_table.rebuild(file_nodes);
+
+	// If no files are selected display the "no file selected" card
+	if (file_nodes == null) {
+	    card_layout.show(this, NO_FILES_SELECTED_CARD);
+	}
+	// If the metadata value table is empty display the "no metadata available" card
+	else if (metadata_value_table.getRowCount() == 0) {
+	    card_layout.show(this, NO_METADATA_AVAILABLE_CARD);
+	}
+	// Otherwise display the card with the metadata value table
+	else {
+	    card_layout.show(this, METADATA_VALUE_TABLE_CARD);
+	}
+    }
+
+
+    private class MetadataValueTable
+	extends JTable
+	implements MetadataChangedListener
+    {
+	private int MINIMUM_TABLE_HEADER_SIZE = 15;
+
+	private MetadataValueTableModel metadata_value_table_model = null;
+	private JTextField metadata_value_text_field = new JTextField();
+
+
+	public MetadataValueTable()
+	{
+            this.setComponentOrientation(Dictionary.getOrientation());
+	    // Create the model for the table
+	    metadata_value_table_model = new MetadataValueTableModel();
+	    setModel(metadata_value_table_model);
+
+	    // We allow only one row in the table to be selected at a time
+	    setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+	    // We use our own editor for the value column so we have easy access to the underlying text field
+	    setDefaultEditor(String.class, new DefaultCellEditor(metadata_value_text_field));
+
+	    // We need to listen for double clicks on the text field to open the editor dialog
+	    metadata_value_text_field.addMouseListener(new MetadataValueTextFieldMouseListener());
+	    metadata_value_text_field.setBorder(null);
+            metadata_value_text_field.setComponentOrientation(Dictionary.getOrientation());
+
+	    // We need to listen for key presses so we can catch Enter presses
+	    addKeyListener(new MetadataValueTableKeyListener());
+
+	    // We need to listen for metadata changes so we can rebuild the table
+	    MetadataXMLFileManager.addMetadataChangedListener(this);
+
+	    // We also have to ensure that the table column header hasn't gone on a severe Jenny Craig binge and has somehow lost 7/8th of its component size
+	    JTableHeader table_header = getTableHeader();
+            table_header.setComponentOrientation(Dictionary.getOrientation());
+            
+	    Dimension table_header_preferred_size = table_header.getPreferredSize();
+	    if (table_header_preferred_size.height < MINIMUM_TABLE_HEADER_SIZE) {
+		table_header_preferred_size.setSize(table_header_preferred_size.width, MINIMUM_TABLE_HEADER_SIZE);
+		table_header.setPreferredSize(table_header_preferred_size);
+	    }
+
+	    // Set the size of the table columns: 2/7 for element, 5/7 for value (actual numbers don't matter) 
+	    TableColumnModel column_model = getColumnModel();
+	    TableColumn inherited_column = column_model.getColumn(0);
+	    inherited_column.setMinWidth(25);
+	    inherited_column.setPreferredWidth(25);
+	    inherited_column.setMaxWidth(25);
+	    TableColumn element_column = column_model.getColumn(1);
+	    element_column.setPreferredWidth(200);
+	    TableColumn value_column = column_model.getColumn(2);
+	    value_column.setPreferredWidth(500);
+
+	    // Use our own renderer for the table cells
+	    MetadataValueTableCellRenderer metadata_value_table_cell_renderer = new MetadataValueTableCellRenderer();
+	    inherited_column.setCellRenderer(metadata_value_table_cell_renderer);
+	    element_column.setCellRenderer(metadata_value_table_cell_renderer);
+	    value_column.setCellRenderer(metadata_value_table_cell_renderer);
+	}
+
+
+	private void addBlankRowForSelectedMetadataElement()
+	{
+	    // Add a blank entry for the selected metadata element, then switch to it
+	    MetadataElement selected_metadata_element = getSelectedMetadataValueTableEntry().getMetadataElement();
+	    int blank_row = metadata_value_table_model.addBlankRowForMetadataElement(selected_metadata_element);
+	    changeSelection(blank_row, 2, false, false);
+	}
+
+
+	private JTextField getMetadataValueTextField()
+	{
+	    return metadata_value_text_field;
+	}
+
+
+	private MetadataValueTableEntry getSelectedMetadataValueTableEntry()
+	{
+	    return metadata_value_table_model.getMetadataValueTableEntry(getSelectedRow());
+	}
+
+
+	private boolean isMouseEventForInheritedMetadataValueTableColumn(MouseEvent mouse_event)
+	{
+	    return (columnAtPoint(mouse_event.getPoint()) == 0);
+	}
+
+
+	public void metadataChanged(CollectionTreeNode[] file_nodes)
+	{
+	    rebuild(file_nodes);
+	}
+
+
+	private void rebuild(CollectionTreeNode[] file_nodes)
+	{
+	    // Note the metadata value table entry currently selected
+	    MetadataValueTableEntry selected_metadata_value_table_entry = getSelectedMetadataValueTableEntry();
+
+	    // We don't want a lot of ListSelectionEvents while the table is rebuilding
+	    clearSelection();
+
+	    // Rebuild the metadata value table model
+	    metadata_value_table_model.rebuild(file_nodes);
+
+	    // Restore the metadata value table entry selection
+	    if (selected_metadata_value_table_entry != null) {
+		int row_to_select = metadata_value_table_model.findMetadataValueTableEntryToSelect(selected_metadata_value_table_entry);
+		changeSelection(row_to_select, 2, false, false);
+	    }
+	}
+
+
+	private void setMetadataValueTextFieldValue(String metadata_value_string)
+	{
+	    metadata_value_text_field.setText(metadata_value_string);
+	    metadata_value_text_field.requestFocus();
+	}
+
+
+	private void stopEditing()
+	{
+	    // Save the current value in the text field, then remove the editor so it doesn't get saved again
+	    TableCellEditor table_cell_editor = getCellEditor();
+	    if (table_cell_editor != null) {
+		Object new_metadata_value = table_cell_editor.getCellEditorValue();
+		setValueAt(new_metadata_value, editingRow, editingColumn);
+		removeEditor();
+	    }
+	}
+
+
+	private class MetadataValueTableCellRenderer
+	    extends DefaultTableCellRenderer
+	{
+	    /** Returns the default table cell renderer.
+	     * @param table The <strong>JTable</strong>.
+	     * @param value The value to assign to the cell at [row, column] as an <strong>Object</strong>.
+	     * @param isSelected <i>true</i> if cell is selected.
+	     * @param hasFocus <i>true</i> if cell has focus.
+	     * @param row The row of the cell to render as an <i>int</i>.
+	     * @param column The column of the cell to render as an <i>int</i>.
+	     * @return The default table cell renderer <strong>Component</strong>.
+	     */
+	    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
+	    {
+		JComponent component = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+                component.setComponentOrientation(Dictionary.getOrientation());
+                
+		int real_column = table.convertColumnIndexToModel(column);
+		// First column: inherited metadata icon
+		if (real_column == 0 && value != null) {
+		    component = new JLabel(JarTools.getImage("upfolder.gif"));
+		    component.setToolTipText(Dictionary.get("EnrichPane.InheritedMetadata_Tooltip"));
+		}
+
+		// Make sure the focus always stay in the value cell of the selected row
+		if (real_column == 2 && isSelected) {
+		    table.editCellAt(row, column);
+		    if (table.isEditing()) {
+			table.getEditorComponent().requestFocus();
+		    }
+		}
+
+		// Set up the component
+		component.setOpaque(true);
+
+		// Foreground
+		if (metadata_value_table_model.isCommon(row)) {
+		    component.setForeground(Color.black);
+		}
+		else {
+		    component.setForeground(Color.gray);
+		}
+
+		// Background
+		if (isSelected) {
+		    component.setBackground(Configuration.getColor("coloring.workspace_heading_background", true));
+		}
+		else {
+		    if (real_column < 2) {
+			component.setBackground(Configuration.getColor("coloring.collection_heading_background", true));
+		    }
+		    else {
+			component.setBackground(Configuration.getColor("coloring.collection_tree_background", true));
+		    }
+		}
+
+		// The value column of cells never paints focus
+		if (real_column == 2) {
+		    component.setBorder(BorderFactory.createEmptyBorder(0,2,0,0));
+		}
+
+		// We set a tooltip over the element column containing the definition of the element
+		if (value instanceof MetadataElement) {
+		    String interface_language_code = Configuration.getLanguage();
+		    String metadata_element_definition = MetadataTools.getMetadataElementAttribute((MetadataElement) value, "definition", interface_language_code, "en");
+		    if (metadata_element_definition != null) {
+			component.setToolTipText(Utility.formatHTMLWidth(metadata_element_definition, 60));
+		    }
+		}
+
+		return component;
+	    }
+	}
+
+
+	private class MetadataValueTableKeyListener
+	    extends KeyAdapter
+	{
+	    /** Gives notification of key events on the text field */
+	    public void keyPressed(KeyEvent key_event)
+	    {
+		// Enter: save the current value then add a blank row for the selected metadata element
+		if (key_event.getKeyCode() == KeyEvent.VK_ENTER) {
+		    // ...but not for extracted metadata elements of course
+		    MetadataValueTableEntry metadata_value_table_entry = getSelectedMetadataValueTableEntry();
+		    if (!metadata_value_table_entry.getMetadataElement().isExtractedMetadataElement()) {
+			addBlankRowForSelectedMetadataElement();
+		    }
+
+		    // We do not want this event to be processed by the table also
+		    key_event.consume();
+		}
+	    }
+	}
+
+
+	private class MetadataValueTextFieldMouseListener
+	    extends MouseAdapter
+	{
+	    public void mouseClicked(MouseEvent mouse_event)
+	    {
+		// Double-click: pop up an editor dialog for large metadata values
+		if (mouse_event.getClickCount() == 2) {
+ 		    EditorDialog editor_dialog = new EditorDialog();
+ 		    String new_metadata_value_string = editor_dialog.display(metadata_value_text_field.getText());
+ 		    if (new_metadata_value_string != null) {
+ 			setMetadataValueTextFieldValue(new_metadata_value_string);
+ 		    }
+		}
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataValueTreePane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataValueTreePane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/MetadataValueTreePane.java	(revision 31635)
@@ -0,0 +1,330 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.gui;
+
+
+import java.awt.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.metadata.MetadataElement;
+import org.greenstone.gatherer.metadata.MetadataValue;
+import org.greenstone.gatherer.metadata.MetadataValueTreeNode;
+import org.greenstone.gatherer.util.PatternTokenizer;
+
+
+/**
+ * This class is a little bit complex, a little bit subtle, and a little bit odd.
+ * The strange interaction model is due to the fact that it is very tightly
+ * coupled to the EnrichPane.
+ *
+ * The interaction is complex because there are three separate controls that the
+ * user may interact with, each of which can affect the other two. The three
+ * controls are:
+ *   - The "assigned metadata" table, at the top right of the meta edit pane.
+ *   - The "metadata value" text field, where users can type in new values.
+ *   - The "metadata value tree", which shows other values that have been
+ *     assigned to the selected metadata element.
+ *
+ * The interactions are:
+ *   - The "assigned metadata" table
+ *     Users may select one (and only one) row in this table. Selecting a row
+ *     chooses one metadata element. The text field will be set to the current
+ *     value of the metadata element. This value will also be selected in the
+ *     metadata value tree.
+ *
+ *   - The "metadata value" text field
+ *     If the value the user has typed in this is a prefix of an entry in the
+ *     value tree, this value will be selected in the value tree. In this case,
+ *     pressing "Tab" will complete the value (ie. make the value in the text
+ *     field the same as the value in the tree). This is to allow users to
+ *     quickly and easily assign existing metadata values to new documents.
+ *
+ *   - The "metadata value tree"
+ *     Selecting a value in the tree will set the text field to this value.
+ */
+public class MetadataValueTreePane
+    extends JPanel
+{
+    private boolean ignore_tree_selection_event = false;
+    /** The metadata value that the tree pane is built on */
+    private MetadataValue metadata_value = null;
+
+    /** Used to display either the MetadataValueTree (when metadata is selected) or a placeholder panel */
+    private CardLayout card_layout = null;
+    /** The name of the panel containing the metadata value tree */
+    private String METADATA_VALUE_TREE_CARD = "";
+    /** The name of the panel containing the "extracted metadata element selected" placeholder */
+    private String EXTRACTED_METADATA_ELEMENT_SELECTED_CARD = "Extracted metadata element selected";
+    /** The name of the panel containing the "inherited metadata selected" placeholder */
+    private String INHERITED_METADATA_SELECTED_CARD = "Inherited metadata selected";
+    /** The name of the panel containing the "not only file only metadata selected" placeholder */
+    private String NOT_ONE_FILE_ONLY_METADATA_SELECTED_CARD = "Not one file only metadata selected";
+    /** The name of the panel containing the "no metadata element selected" placeholder */
+    private String NO_METADATA_ELEMENT_SELECTED_CARD = "No metadata element selected";
+
+    private JLabel metadata_value_tree_label = new JLabel();
+    private JTextArea extracted_metadata_element_selected_message;
+    private JTree metadata_value_tree;
+
+
+    public MetadataValueTreePane()
+    {
+	super();
+        JScrollPane scrol_tmp;
+        this.setComponentOrientation(Dictionary.getOrientation());
+        metadata_value_tree_label.setComponentOrientation(Dictionary.getOrientation());
+	// Card containing the metadata value tree
+	metadata_value_tree = new JTree();
+        metadata_value_tree.setComponentOrientation(Dictionary.getOrientation());
+	metadata_value_tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+	metadata_value_tree.setModel(null);
+	metadata_value_tree.setRootVisible(false);
+	metadata_value_tree.putClientProperty("JTree.lineStyle", "Angled");
+
+	JPanel metadata_value_tree_pane = new JPanel();
+        metadata_value_tree_pane.setComponentOrientation(Dictionary.getOrientation());        
+	metadata_value_tree_pane.setLayout(new BorderLayout());
+	metadata_value_tree_pane.add(metadata_value_tree_label, BorderLayout.NORTH);
+        scrol_tmp = new JScrollPane(metadata_value_tree);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	metadata_value_tree_pane.add(scrol_tmp, BorderLayout.CENTER);
+
+	// Card containing the "extracted metadata element selected" message
+	extracted_metadata_element_selected_message = new JTextArea("");
+        extracted_metadata_element_selected_message.setComponentOrientation(Dictionary.getOrientation());
+	extracted_metadata_element_selected_message.setEditable(false);
+	extracted_metadata_element_selected_message.setLineWrap(true);
+	extracted_metadata_element_selected_message.setOpaque(false);
+	extracted_metadata_element_selected_message.setWrapStyleWord(true);
+
+	JPanel extracted_metadata_element_selected_pane = new JPanel();
+        extracted_metadata_element_selected_pane.setComponentOrientation(Dictionary.getOrientation());
+	extracted_metadata_element_selected_pane.setBorder(BorderFactory.createEmptyBorder(25,0,0,0));
+	extracted_metadata_element_selected_pane.setLayout(new BorderLayout());
+	extracted_metadata_element_selected_pane.add(extracted_metadata_element_selected_message, BorderLayout.CENTER);
+
+	// Card containing the "inherited metadata selected" message
+	JTextArea inherited_metadata_selected_message = new JTextArea(Dictionary.get("EnrichPane.InheritedMetadataSelected"));
+	inherited_metadata_selected_message.setEditable(false);
+	inherited_metadata_selected_message.setLineWrap(true);
+	inherited_metadata_selected_message.setOpaque(false);
+	inherited_metadata_selected_message.setWrapStyleWord(true);
+        inherited_metadata_selected_message.setComponentOrientation(Dictionary.getOrientation());
+	
+	JPanel inherited_metadata_selected_pane = new JPanel();
+	inherited_metadata_selected_pane.setBorder(BorderFactory.createEmptyBorder(25,0,0,0));
+	inherited_metadata_selected_pane.setLayout(new BorderLayout());
+	inherited_metadata_selected_pane.add(inherited_metadata_selected_message, BorderLayout.CENTER);
+        inherited_metadata_selected_pane.setComponentOrientation(Dictionary.getOrientation());
+	// Card containing the "not one file only metadata selected" message
+	JTextArea not_one_file_only_metadata_selected_message = new JTextArea(Dictionary.get("EnrichPane.NotOneFileOnlyMetadataSelected"));
+	not_one_file_only_metadata_selected_message.setEditable(false);
+	not_one_file_only_metadata_selected_message.setLineWrap(true);
+	not_one_file_only_metadata_selected_message.setOpaque(false);
+	not_one_file_only_metadata_selected_message.setWrapStyleWord(true);
+        not_one_file_only_metadata_selected_message.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel not_one_file_only_metadata_selected_pane = new JPanel();
+	not_one_file_only_metadata_selected_pane.setBorder(BorderFactory.createEmptyBorder(25,0,0,0));
+	not_one_file_only_metadata_selected_pane.setLayout(new BorderLayout());
+	not_one_file_only_metadata_selected_pane.add(not_one_file_only_metadata_selected_message, BorderLayout.CENTER);
+        not_one_file_only_metadata_selected_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	// Card containing the "no metadata element selected" message
+	JLabel no_metadata_element_selected_label = new JLabel(Dictionary.get("EnrichPane.No_Metadata_Element"));
+	no_metadata_element_selected_label.setHorizontalAlignment(JLabel.CENTER);
+	no_metadata_element_selected_label.setOpaque(false);
+	no_metadata_element_selected_label.setVerticalAlignment(JLabel.CENTER);
+        no_metadata_element_selected_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel no_metadata_element_selected_pane = new JPanel();
+	no_metadata_element_selected_pane.setLayout(new BorderLayout());
+	no_metadata_element_selected_pane.add(no_metadata_element_selected_label, BorderLayout.CENTER);
+        no_metadata_element_selected_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	card_layout = new CardLayout();
+        
+	this.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	this.setFont(Configuration.getFont("general.font", false));
+	this.setLayout(card_layout);
+	this.add(no_metadata_element_selected_pane, NO_METADATA_ELEMENT_SELECTED_CARD);
+	this.add(extracted_metadata_element_selected_pane, EXTRACTED_METADATA_ELEMENT_SELECTED_CARD);
+	this.add(inherited_metadata_selected_pane, INHERITED_METADATA_SELECTED_CARD);
+	this.add(not_one_file_only_metadata_selected_pane, NOT_ONE_FILE_ONLY_METADATA_SELECTED_CARD);
+	this.add(metadata_value_tree_pane, METADATA_VALUE_TREE_CARD);
+    }
+
+
+    public void addMetadataValueTreeSelectionListener(TreeSelectionListener tree_selection_listener)
+    {
+	metadata_value_tree.addTreeSelectionListener(tree_selection_listener);
+    }
+
+
+    /** Returns a TreePath for the node most closely matching the metadata value. */
+    private TreePath getClosestPath(String metadata_value_string)
+    {
+	if (metadata_value_tree.getModel() == null) {
+	    return null;
+	}
+
+	// Start at the root of the tree
+	MetadataValueTreeNode tree_node = (MetadataValueTreeNode) metadata_value_tree.getModel().getRoot();
+
+	// Separate hierarchical values
+	PatternTokenizer tokenizer = new PatternTokenizer(metadata_value_string, MetadataValueTreeNode.METADATA_VALUE_TREE_NODE_HIERARCHY_TOKEN);
+	while (tokenizer.hasMoreTokens()) {
+	    String token = tokenizer.nextToken();
+
+	    // All components except the last must match exactly
+	    if (tokenizer.hasMoreTokens()) {
+		for (int i = 0; i < tree_node.getChildCount(); i++) {
+		    MetadataValueTreeNode child_node = (MetadataValueTreeNode) tree_node.getChildAt(i);
+		    if (child_node.getValue().equals(token)) {
+			// The matching node has been found, so move onto the next token
+			tree_node = child_node;
+			break;
+		    }
+		}
+	    }
+
+	    // The last component may match partially
+	    else {
+		for (int i = 0; i < tree_node.getChildCount(); i++) {
+		    MetadataValueTreeNode child_node = (MetadataValueTreeNode) tree_node.getChildAt(i);
+		    if (child_node.getFullValue().startsWith(metadata_value_string)) {
+			// The closest node has been found, so return its path
+			return new TreePath(child_node.getPath());
+		    }
+		}
+
+		// Not even a partial match
+		return null;
+	    }
+	}
+
+	// If nothing else, return the path of the root node
+	return new TreePath(tree_node.getPath());
+    }
+
+
+    public MetadataValueTreeNode getSelectedMetadataValueTreeNode()
+    {
+	if (metadata_value_tree.getSelectionCount() == 0 || ignore_tree_selection_event) {
+	    return null;
+	}
+
+	return (MetadataValueTreeNode) metadata_value_tree.getSelectionPath().getLastPathComponent();
+    }
+
+
+    public void rebuild(MetadataValue new_metadata_value)
+    {
+	// If the metadata value hasn't changed there is nothing to do
+	if (new_metadata_value == metadata_value) {
+	    return;
+	}
+
+	MetadataElement metadata_element = ((metadata_value != null) ? metadata_value.getMetadataElement() : null);
+	metadata_value = new_metadata_value;
+
+	// Selection cleared, so display "no metadata element selected" card and we're done
+	if (new_metadata_value == null) {
+	    metadata_value_tree.setModel(null);
+	    card_layout.show(this, NO_METADATA_ELEMENT_SELECTED_CARD);
+	    return;
+	}
+
+	// If a piece of inherited metadata is selected, display "inherited metadata selected" card and we're done
+	if (new_metadata_value.isInheritedMetadata()) {
+	    card_layout.show(this, INHERITED_METADATA_SELECTED_CARD);
+	    return;
+	}
+
+	// If the metadata applies to multiple files, display "not one file only metadata selected" card and we're done
+	if (new_metadata_value.isOneFileOnlyMetadata() == false) {
+	    card_layout.show(this, NOT_ONE_FILE_ONLY_METADATA_SELECTED_CARD);
+	    return;
+	}
+
+	// If an extracted metadata element is selected, display "extracted metadata element selected" card
+	MetadataElement new_metadata_element = new_metadata_value.getMetadataElement();
+	if (new_metadata_element.isExtractedMetadataElement()) {
+	    String[] args = new String[1];
+	    args[0] = new_metadata_element.getDisplayName();
+	    extracted_metadata_element_selected_message.setText(Dictionary.get("EnrichPane.AutoMessage", args));
+	    card_layout.show(this, EXTRACTED_METADATA_ELEMENT_SELECTED_CARD);
+	    return;
+	}
+
+	// Display the value tree for the selected metadata element
+	String[] args = new String[1];
+	args[0] = new_metadata_element.getDisplayName();
+	metadata_value_tree_label.setText(Dictionary.get("EnrichPane.ExistingValues", args));
+	
+	metadata_value_tree.setModel(new_metadata_element.getMetadataValueTreeModel());
+	card_layout.show(this, METADATA_VALUE_TREE_CARD);
+	selectBestPathForMetadataValue(new_metadata_value.getFullValue());
+    }
+
+
+    public void selectBestPathForMetadataValue(String metadata_value_string)
+    {
+	TreePath best_path = getClosestPath(metadata_value_string);
+
+	// Select the new path in the tree
+	// The tree selection event this causes must be ignored, since it alters the metadata text field value
+	ignore_tree_selection_event = true;
+	metadata_value_tree.setSelectionPath(best_path);
+	ignore_tree_selection_event = false;
+
+	// If a folder has been specified, make sure it is expanded
+	if (metadata_value_string.endsWith(MetadataValueTreeNode.METADATA_VALUE_TREE_NODE_HIERARCHY_TOKEN) && !metadata_value_tree.isExpanded(best_path)) {
+	    metadata_value_tree.expandPath(best_path);
+	}
+
+	// Make sure the tree path is expanded
+	metadata_value_tree.makeVisible(best_path);
+
+	// Make sure the tree path is visible on screen
+	final Rectangle bounds = metadata_value_tree.getPathBounds(best_path);
+	if (bounds != null) {
+	    bounds.x = 0;  // Want the tree to be always flushed left
+
+	    // Scrolling the path to be visible must be done on the GUI thread
+	    Runnable task = new Runnable() {
+		    public void run() {
+			metadata_value_tree.scrollRectToVisible(bounds);
+		    }
+		};
+	    SwingUtilities.invokeLater(task);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ModalDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ModalDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ModalDialog.java	(revision 31635)
@@ -0,0 +1,218 @@
+/*#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+
+
+/** An extension of the JDialog that overrides the JVM's typical modal behaviour. This typical behaviour is that when a modal dialog is opened, all other windows cease to respond to user events until the modal dialog is disposed. However this prevents us opening the help documents property whenever a modal dialog is open. Thus we override the modal behaviour so that only the owner frame or dialog is blocked. 
+ * Note that because we always call the super constructor with modal set to false, this should be made visible with setVisible(true) rather than show() which will return straight away. */
+// feedback note: veronika had changed all the super constructor calls to 
+// use modal instead of false - not sure if this is needed so I have not 
+// put that in. --kjdon
+public class ModalDialog
+    extends JDialog {
+
+    /** The current modal dialog being shown on screen, if any. */
+    static public ModalDialog current_modal = null;
+
+    /** true if this dialog should be modal, ie block user actions to its owner window, false otherwise. */
+    protected boolean modal = false;
+    /** true if this dialog is currently waiting some thread. */
+    protected boolean waiting = false;
+
+    /** Constructor.
+     */
+    public ModalDialog() {
+	super((Frame)null, "", false);
+        this.setComponentOrientation(Dictionary.getOrientation());
+    }
+
+    /** Constructor.
+     * @param parent the Dialog which is the owener of this dialog.
+    */
+    public ModalDialog(Dialog parent) {
+	super(parent, "", false);
+        this.setComponentOrientation(Dictionary.getOrientation());
+    }
+
+    /** Constructor.
+     * @param parent the Dialog which is the owener of this dialog.
+     * @param modal true if this dialog should be modal, ie block user actions to its owner window, false otherwise.
+     */
+    public ModalDialog(Dialog parent, boolean modal) {
+	super(parent, "", false);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	this.modal = modal;
+    }
+    
+    /** Constructor.
+     * @param parent the Dialog which is the owner of this dialog.
+     * @param title the String to use as the title for this dialog.
+     */
+    public ModalDialog(Dialog parent, String title) {
+	super (parent, title, false);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	this.modal = false;
+    }
+
+    /** Constructor.
+     * @param parent the Dialog which is the owener of this dialog.
+     * @param title the String to use as the title for this dialog.
+     * @param modal true if this dialog should be modal, ie block user actions to its owner window, false otherwise.
+     */
+    public ModalDialog(Dialog parent, String title, boolean modal) {
+	super (parent, title, false);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	this.modal = modal;
+    }
+
+   /** Constructor.
+     * @param parent the Frame which is the owener of this dialog.
+     */
+    public ModalDialog(Frame parent) {
+	super(parent, "", false);
+        this.setComponentOrientation(Dictionary.getOrientation());
+    }
+
+    /** Constructor.
+     * @param parent the Frame which is the owener of this dialog.
+     * @param modal whether this dialog is modal or not
+     */
+    public ModalDialog(Frame parent, boolean modal) {
+	super(parent, "", false);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	this.modal = modal;
+    }
+    
+    /** Constructor.
+     * @param parent the Frame which is the owner of this dialog.
+     * @param title the String to use as the title for this dialog.
+     */
+    public ModalDialog(Frame parent, String title) {
+	super (parent, title, false);
+        this.setComponentOrientation(Dictionary.getOrientation());
+    }
+
+    /** Constructor.
+     * @param parent the Frame which is the owener of this dialog.
+     * @param title the String to use as the title for this dialog.
+     * @param modal true if this dialog should be modal, ie block user actions to its owner window, false otherwise.
+     */
+    public ModalDialog(Frame parent, String title, boolean modal) {
+	super (parent, title, false);
+	this.modal = modal;
+        this.setComponentOrientation(Dictionary.getOrientation());
+    }
+
+    public void dispose() {
+	super.dispose();
+    }
+
+    /** Ensures the current dialog is visible. */
+    public void makeVisible() {
+	super.setVisible(true);
+    }
+
+    /** The set visible method is overriden to provide modal functionality. It essentially hijacks control of the event dispatch thread while the dialog is open, only allowing non-user events to be passed to the parent dialog. Furthermore it only has this effect within the current AWT component tree by utilitizing the TreeLock.
+     * @param visible true if this dialog should be painted on-screen, false otherwise.
+     */
+    public void setVisible(boolean visible)
+    {
+	if (visible) {
+	    current_modal = this;
+	}
+	else {
+	    current_modal = null;
+	}
+
+	// If we are in the AWT Dispatch thread then it is all good.
+ 	if (SwingUtilities.isEventDispatchThread()) {
+ 	    super.setVisible(visible);
+	    if (modal && visible) {
+		EventQueue theQueue = getToolkit().getSystemEventQueue();
+		while (isVisible()) {
+		    try {
+			AWTEvent event = theQueue.getNextEvent();
+			Object src = event.getSource();
+
+			// Block all keyboard and mouse events to the parent component
+			if (src.equals(getParent())) {
+			    if (event instanceof KeyEvent || event instanceof MouseEvent) {
+				// System.err.println("Event to parent component blocked.");
+				continue;
+			    }
+			}
+
+			// Re-dispatch other events
+			if (event instanceof ActiveEvent) {
+  			    ((ActiveEvent) event).dispatch();
+  			}			    
+			else if (src instanceof Component) {
+			    ((Component) src).dispatchEvent(event);
+			}
+		    }
+		    catch (Exception exception) {
+			DebugStream.printStackTrace(exception);
+		    }
+		}
+	    }
+ 	}
+ 	else {
+ 	    try {
+ 		SwingUtilities.invokeAndWait(new MakeDialogVisibleTask(this, visible));
+ 	    }
+ 	    catch (Exception exception) {
+ 		DebugStream.printStackTrace(exception);
+ 	    }
+ 	}
+    }
+    
+    private class MakeDialogVisibleTask
+	implements Runnable {
+	private boolean make_visible;
+	private ModalDialog dialog;
+	public MakeDialogVisibleTask(ModalDialog dialog, boolean make_visible) {
+	    this.dialog = dialog;
+	    this.make_visible = make_visible;
+	}
+	public void run() {
+	    // Blocks until the user dismisses the dialog
+	    dialog.setVisible(make_visible);
+	}
+    }
+
+    /** Overridden method so we can control modality and not rely on the Dialog default.
+     * @param modal true if this dialog should be modal, ie block user actions to its owner window, false otherwise.
+     */
+    public void setModal (boolean modal) {
+	this.modal = modal;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ModalProgressPopup.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ModalProgressPopup.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ModalProgressPopup.java	(revision 31635)
@@ -0,0 +1,92 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.gui;
+
+
+import java.awt.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+
+
+public class ModalProgressPopup
+    extends ModalDialog
+{
+    /** The size of the popup */
+    final static private Dimension SIZE = new Dimension(300, 75);
+
+    private String message = null;
+
+
+    public ModalProgressPopup(String title, String message)
+    {
+	super(Gatherer.g_man, title);
+	this.message = message;
+        this.setComponentOrientation(Dictionary.getOrientation());
+    }
+
+
+    /** Method to close the popup. */
+    public void close()
+    {
+	setVisible(false);
+	Gatherer.g_man.wait(false);
+    }
+
+
+    /** Method to display the popup on screen. */
+    public void display()
+    {
+	setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+	setSize(SIZE);
+
+	// Create
+	JProgressBar progress_bar = new JProgressBar();
+        progress_bar.setComponentOrientation(Dictionary.getOrientation());
+	progress_bar.setIndeterminate(true);
+
+	// Layout
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+        
+        JLabel tmp = new JLabel(message);
+        tmp.setComponentOrientation(Dictionary.getOrientation());
+	
+        content_pane.add(tmp, BorderLayout.NORTH);
+	content_pane.add(progress_bar, BorderLayout.CENTER);
+
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(true);
+
+	// Lock the rest of the GLI interface until close() is called
+	Gatherer.g_man.wait(true);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NewCollectionDetailsPrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NewCollectionDetailsPrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NewCollectionDetailsPrompt.java	(revision 31635)
@@ -0,0 +1,728 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Project, NZDL, University of Waikato
+ *
+ * Copyright (C) 2003 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.File;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+import javax.swing.filechooser.FileFilter;
+import javax.swing.filechooser.FileView;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.BasicCollectionConfiguration;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.JarTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+public class NewCollectionDetailsPrompt
+    extends ModalDialog {
+
+    
+    static public boolean titleClashes(String collectDir, String title, File current_config_file) {
+	// An empty collection title never clashes with anything. It may look ugly in the final collection but there is nothing wrong with having no title for a particular language.
+	if(title == null || title.length() == 0) {
+	    return false;
+	}
+	File collect_directory = new File(collectDir);
+	String file_name = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+	File children[] = collect_directory.listFiles();
+	for(int i = 0; children != null && i < children.length; i++) {
+	    if(children[i].isDirectory()) {
+		File config_file = new File(children[i], file_name);
+		if(current_config_file == null || !config_file.equals(current_config_file)) {
+		    BasicCollectionConfiguration other_collection = new BasicCollectionConfiguration(config_file);
+		    if(other_collection.getName().equalsIgnoreCase(title)) {  
+			return true;
+		    }
+		    other_collection = null;
+		}
+		config_file = null;
+	    }
+	}
+	return false;
+    }
+
+    private boolean cancelled;
+    private boolean collectDirChanged;
+    private File base_final;
+    private JButton chdir_button;
+    private JButton create_button;
+    private JComboBox base_collection;
+    private JDialog self;
+    private JRadioButton personal_collection_button = null;
+    private JTextArea description;
+    private JTextField title;
+    private String collectBasePath;
+    private String newCollectPath;
+    private String description_final;
+    private String title_final="";
+
+    static private Dimension COMPONENT_SIZE = new Dimension(230, 25);
+    /** The size of this new collection dialog box. */
+    static private Dimension SIZE = new Dimension(600, 280);
+    static private int FILENAME_SIZE = 8;
+
+    /** Constructor.
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    public NewCollectionDetailsPrompt() {
+	super(Gatherer.g_man, true);        
+	this.cancelled = true;
+        this.setComponentOrientation(Dictionary.getOrientation());
+	this.self = this;
+	newCollectPath = Gatherer.getCollectDirectoryPath();
+	collectBasePath = null;
+	collectDirChanged = false;
+
+	// Setup
+	setJMenuBar(new SimpleMenuBar("creatingacollection"));
+	setSize(SIZE);
+	setTitle(Dictionary.get("NewCollectionPrompt.Title"));
+	
+	// Model building. Build a model of all of the collections in the gsdl collect directory with the appropriate directories.
+	Vector base_collection_model = new Vector();
+	setupBaseCollections(base_collection_model, null); // if no collect directory passed in: use default collect directory
+
+
+	// Creation
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setOpaque(true);
+	JPanel upper_pane = new JPanel();
+        upper_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel instructions_label = new JLabel(Dictionary.get("NewCollectionPrompt.Instructions"));
+	instructions_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel title_pane = new JPanel();
+        title_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel title_label = new JLabel(Dictionary.get("CDM.General.Collection_Name"));
+        title_label.setComponentOrientation(Dictionary.getOrientation());
+	title = new JTextField();
+        title.setComponentOrientation(Dictionary.getOrientation());
+	title.setPreferredSize(COMPONENT_SIZE);
+	title.setToolTipText(Dictionary.get("CDM.General.Collection_Name_Tooltip"));
+	JLabel name_label = new JLabel(Dictionary.get("NewCollectionPrompt.Collection_Name"));
+	name_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel center_pane = new JPanel();
+        center_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel description_pane = new JPanel();
+        description_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel description_label = new JLabel(Dictionary.get("NewCollectionPrompt.Collection_Description"));
+        description_label.setComponentOrientation(Dictionary.getOrientation());
+	description = new JTextArea();
+        description.setComponentOrientation(Dictionary.getOrientation());
+	description.setBackground(Configuration.getColor("coloring.editable_background", false));
+	description.setForeground(Configuration.getColor("coloring.editable_foreground", false));
+	description.setRows(5);
+	description.setToolTipText(Dictionary.get("CDM.General.Collection_Extra_Tooltip"));
+	
+	JPanel bottom_pane = new JPanel();
+        bottom_pane.setComponentOrientation(Dictionary.getOrientation());
+	// Base Collection
+	JPanel base_collection_pane = new JPanel();
+        base_collection_pane.setComponentOrientation(Dictionary.getOrientation());
+	
+        JLabel base_collection_label = new JLabel(Dictionary.get("NewCollectionPrompt.Base_Collection"));
+        base_collection_label.setComponentOrientation(Dictionary.getOrientation());
+	
+        base_collection = new JComboBox(base_collection_model);
+        base_collection.setComponentOrientation(Dictionary.getOrientation());    
+	base_collection.setOpaque(false);
+	base_collection.setToolTipText(Dictionary.get("NewCollectionPrompt.Base_Collection_Tooltip"));
+	
+	JPanel collection_scope_pane = new JPanel();
+        collection_scope_pane.setComponentOrientation(Dictionary.getOrientation());
+	
+        personal_collection_button = new JRadioButton(Dictionary.get("NewCollectionPrompt.Collection_Scope_Personal"));
+        personal_collection_button.setToolTipText(Dictionary.get("NewCollectionPrompt.Collection_Scope_Personal_Tooltip"));
+	personal_collection_button.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	personal_collection_button.setOpaque(false);
+        personal_collection_button.setComponentOrientation(Dictionary.getOrientation());
+	
+        JRadioButton shared_collection_button = new JRadioButton(Dictionary.get("NewCollectionPrompt.Collection_Scope_Shared"));        
+	shared_collection_button.setToolTipText(Dictionary.get("NewCollectionPrompt.Collection_Scope_Shared_Tooltip")); 
+	shared_collection_button.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	shared_collection_button.setOpaque(false);
+        shared_collection_button.setComponentOrientation(Dictionary.getOrientation());
+	
+        ButtonGroup collection_scope_group = new ButtonGroup();
+	collection_scope_group.add(personal_collection_button);
+	collection_scope_group.add(shared_collection_button);
+	personal_collection_button.setSelected(true);
+
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	create_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+	chdir_button = new GLIButton(Dictionary.get("General.CD"), Dictionary.get("General.CD_Tooltip"));
+	JButton cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+
+	// Connection
+	chdir_button.addActionListener(new ChangeDirListener());
+	if(Gatherer.isGsdlRemote) { // for client GLI disable changing directories
+	    chdir_button.setEnabled(false);	    
+	} else { // can't base collections on any in OTHER collect dirs in client GLI
+	    base_collection.addActionListener(new OtherCollectionsListener());
+	}
+	cancel_button.addActionListener(new CancelListener());
+	create_button.addActionListener(new CreateListener());
+	description.addKeyListener(new DescriptionListener());
+
+	// Layout
+	title_pane.setLayout(new BorderLayout(5,0));
+	title_pane.add(title_label, BorderLayout.LINE_START);
+	title_pane.add(title, BorderLayout.CENTER);
+
+	upper_pane.setLayout(new GridLayout(2,1));
+	upper_pane.add(instructions_label);
+	upper_pane.add(title_pane);
+
+        JScrollPane scrol_tmp;
+        
+	description_pane.setLayout(new BorderLayout());
+	description_pane.add(description_label, BorderLayout.NORTH);        
+        scrol_tmp=new JScrollPane(description);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	description_pane.add(scrol_tmp, BorderLayout.CENTER);
+
+	base_collection_pane.setLayout(new BorderLayout(5,0));
+	base_collection_pane.add(base_collection_label, BorderLayout.LINE_START);
+	base_collection_pane.add(base_collection, BorderLayout.CENTER);
+
+	collection_scope_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	collection_scope_pane.setLayout(new GridLayout(1,2));
+	collection_scope_pane.add(personal_collection_button);
+	collection_scope_pane.add(shared_collection_button);
+
+	center_pane.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));
+	center_pane.setLayout(new BorderLayout());
+	center_pane.add(description_pane, BorderLayout.CENTER);
+
+	bottom_pane.setLayout(new BorderLayout());
+	bottom_pane.add(base_collection_pane, BorderLayout.NORTH);
+	if (Gatherer.isGsdlRemote) {
+	    bottom_pane.add(collection_scope_pane, BorderLayout.CENTER);
+	    bottom_pane.add(button_pane, BorderLayout.SOUTH);
+	}
+	else {
+	    bottom_pane.add(button_pane, BorderLayout.CENTER);
+	}
+
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	button_pane.setLayout(new GridLayout(1,3));
+	button_pane.add(create_button);
+	button_pane.add(chdir_button);
+	button_pane.add(cancel_button);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(upper_pane, BorderLayout.NORTH);
+	content_pane.add(center_pane, BorderLayout.CENTER);
+	content_pane.add(bottom_pane, BorderLayout.SOUTH);
+	// Final dialog setup & positioning.
+	getRootPane().setDefaultButton(create_button);
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(true);
+    }
+
+    public boolean isCancelled() {
+	return cancelled;
+    }
+
+    public File getBase() {
+	return base_final;
+    }
+
+    public String getDescription() {
+	return description_final;
+    }
+ 
+    /** Generates the collection short filename by taking the first eight characters of the title (spaces removed) and then adjusting by further truncating and then adding an unique suffix as necessary.
+     * @return the filename as a String
+     */
+    public String getName()
+    {
+	// Retrieve the first 8 non-whitespace ASCII characters of title_final.
+	StringBuffer name_buffer = new StringBuffer("");
+	for (int i = 0, u = 0; (i < title_final.length() && u < 8); i++) {
+	    // To be safe we make sure the internal collection name (folder name) contains only ASCII characters
+	    char c = title_final.charAt(i);
+	    if ((int) c < 128 && Character.isLetterOrDigit(c)) {
+		name_buffer.append(Character.toLowerCase(c));
+		u++;
+	    }
+	}
+
+	// Use "col" as the base collection name if we have nothing left
+	if (name_buffer.length() == 0) {
+	    name_buffer = new StringBuffer("col");
+	}
+
+	// Remote collections that aren't shared have the user's username prefixed to the collection name
+	if (Gatherer.isGsdlRemote && personal_collection_button.isSelected()) {
+	    name_buffer = new StringBuffer(Gatherer.remoteGreenstoneServer.getUsername() + "-" + name_buffer.toString());
+	}
+
+	// We need to ensure the filename is unique
+	int counter = 0;
+	StringBuffer new_name_buffer = new StringBuffer(name_buffer.toString());
+	while(filenameClashes(new_name_buffer.toString())) {
+	    new_name_buffer = new StringBuffer(name_buffer.toString());
+	    counter++;
+	    String suffix = String.valueOf(counter);
+	    // If we have to truncate the namestring so as to fit the suffix
+	    if(suffix.length() + new_name_buffer.length() > 8) {
+		new_name_buffer.replace(new_name_buffer.length() - suffix.length(), new_name_buffer.length(), suffix);
+	    }
+	    // Or just append it if that isn't necessary
+	    else {
+		new_name_buffer.append(suffix);
+	    }
+	}
+
+	// All done
+	return new_name_buffer.toString();
+    }
+    
+    private boolean filenameClashes(String filename)
+    {
+	File collect_directory = new File(Gatherer.getCollectDirectoryPath());
+	File children[] = collect_directory.listFiles();
+	for(int i = 0; children != null && i < children.length; i++) {
+	    if(children[i].getName().equals(filename)) {
+		return true;
+	    }
+	}
+	return false;
+    }
+
+    public String getTitle() {
+	return title_final;
+    }
+
+    private void setupBaseCollections(Vector base_collection_model, String collectDirectory) {
+
+	File collect_directory = null;
+	if(collectDirectory != null) {
+	    collect_directory = new File(collectDirectory);
+	}
+
+	// need to modify this to base a coll on any collection from any site
+	if (Gatherer.GS3 && !Gatherer.isGsdlRemote) {
+	    File sites_dir = new File(Gatherer.getSitesDirectoryPath());
+	    File [] sites = sites_dir.listFiles();
+	    for (int i=0; i<sites.length; i++) {
+		if(collect_directory == null) {
+		    collect_directory = new File(sites_dir + File.separator + sites[i].getName() + File.separator + "collect");
+		} 
+		if (collect_directory.exists()) {
+		    addCollectionsToModel(base_collection_model, collect_directory, sites[i].getName());
+		}
+		collect_directory = null;
+	    }
+	} else {
+	    if(collect_directory == null) { // if no collect directory passed in: use default collect directory
+		collect_directory = new File(Gatherer.getCollectDirectoryPath());
+	    }
+	   addCollectionsToModel(base_collection_model, collect_directory, null);
+	}
+
+	// Sort the result.
+	Collections.sort(base_collection_model);
+	base_collection_model.add(0, new Item(null, Dictionary.get("NewCollectionPrompt.NewCollection")));
+	
+	// last item allows one to open up collections from other collect folders to base the new one on
+	if(!Gatherer.isGsdlRemote) { // Not yet implemented for client GLI
+	    base_collection_model.add(new Item(null, Dictionary.get("NewCollectionPrompt.OtherCollections")));
+	}
+    }
+
+    private void addCollectionsToModel(Vector base_collection_model, File collect_directory, String site) {
+	File[] possible_collections = collect_directory.listFiles();
+	String file_name = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+	for (int i = 0; possible_collections != null && i < possible_collections.length; i++) {
+	    // If the directory has a etc/collect.cfg file then it looks like a collection
+	    File collect_cfg_file = new File(possible_collections[i], file_name);
+	    if (collect_cfg_file.exists()) {
+		// Check for group coll: Check the collect.cfg for collectgroup=true
+		BasicCollectionConfiguration config = new BasicCollectionConfiguration(collect_cfg_file);
+		if (config.getCollectGroup().equals("true")) {
+		    // try subdirs in here
+		    addCollectionsToModel(base_collection_model, possible_collections[i], site);
+		} 
+		else {
+		    
+		    // If the directory has a metadata/ then the collection can be used as a base
+		    File metadata_directory = new File(possible_collections[i], "metadata");
+		    if (metadata_directory.exists()) {
+			// Add to model
+			BasicCollectionConfiguration collect_cfg = new BasicCollectionConfiguration(collect_cfg_file);
+			if (Gatherer.GS3 && site != null) {
+			    collect_cfg.setSite(site);
+			}
+			Item item = new Item(possible_collections[i], collect_cfg);
+			if (!base_collection_model.contains(item)) {
+			    base_collection_model.add(item);
+			}
+		    }
+		}
+	    }
+	}
+       
+    }
+
+    private class OtherCollectionsListener implements ActionListener {
+
+	// the JComboBox listener will receive an ActionEvent 
+	// when a selection has been made
+
+	public void actionPerformed(ActionEvent event) {
+
+	    // If the Other Collections item was selected (which is the last one in the list)
+	    if(base_collection.getSelectedIndex() == base_collection.getItemCount()-1) {
+
+		// open a filechooser in whatever is the current collect directory
+		// within the NewCollectionDetailsPrompt's settings
+		if(collectBasePath == null) {
+		    collectBasePath = newCollectPath;
+		}
+
+		JFileChooser chooser = new JFileChooser(collectBasePath);
+		chooser.setFileView(new CollectionFileView());
+		chooser.setDialogTitle(Dictionary.get("NewCollectionPrompt.ChooseACollection"));
+		chooser.setApproveButtonText(Dictionary.get("NewCollectionPrompt.Select"));
+		
+		// to prevent the user from traversing into collection folders,
+		// need to:
+		// - NOT set the filechooser to Directories only, but files and directories
+		// (so that they can still select directories)
+		// - add a filefilter that accepts only directories (i.e. rejects files)
+		// - remove the accept all filter
+		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+		//chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+		chooser.setFileFilter(new CollectionFileFilter());
+		chooser.setAcceptAllFileFilterUsed(false);
+
+		int returnVal = chooser.showOpenDialog(NewCollectionDetailsPrompt.this);
+		if(returnVal == JFileChooser.APPROVE_OPTION) {
+
+		    File selectedFile = chooser.getSelectedFile(); 
+		    
+		    // don't bother reloading if nothing has changed
+		    if(collectBasePath.equals(selectedFile.getAbsolutePath() + File.separator)) { 
+			// don't want Other Collections... selected
+			base_collection.setSelectedIndex(0);
+		    } else {
+
+			// If the selected directory is a collection folder get its collect dir,
+			// make collectBasePath the collect dir, and select the collection itself
+			String cfg = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+			boolean isCollection = false;
+			while(new File(selectedFile, cfg).exists()) { // while loop handles collectgroup
+			    selectedFile = selectedFile.getParentFile();
+			    isCollection = true;
+			} // else may be a collect dir, in which case leave it as it is again
+
+			// found collect directory for base collections
+			collectBasePath = selectedFile.getAbsolutePath()+File.separator; 
+
+			// change the available collections to base this collection on
+			base_collection.removeAllItems();
+			Vector base_collection_model = new Vector();
+			setupBaseCollections(base_collection_model, collectBasePath);
+			for(int i = 0; i < base_collection_model.size(); i++) {
+			    Item item = (Item)base_collection_model.get(i);
+			    base_collection.addItem(item);
+
+			    // select the originally chosen collection, if any, in the combobox
+			    if(isCollection && chooser.getSelectedFile().equals(item.getFile())) {
+				base_collection.setSelectedIndex(i);
+			    }
+			}
+			
+			// But if the user selected a collect group, set combobox to New Collection
+			if(!isCollection) {
+			    // if it was a collectgroup, nothing new would be selected, set to New Coll			    
+			    if(base_collection.getSelectedIndex() == base_collection.getItemCount()-1) {
+				base_collection.setSelectedIndex(0);
+			    }
+			}
+
+		    } 
+
+		} else { // cancelled out of dialog, so jump to New Collection
+		    base_collection.setSelectedIndex(0);
+		}		
+	    }
+	}
+    }
+
+    private class ChangeDirListener implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    String oldCollectPath = newCollectPath;
+	    JFileChooser chooser = new JFileChooser(newCollectPath);
+	    chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+	    chooser.setDialogTitle(Dictionary.get("General.ChooseCollectDirectory"));
+
+	    int returnVal = chooser.showOpenDialog(NewCollectionDetailsPrompt.this);
+	    if(returnVal == JFileChooser.APPROVE_OPTION) {
+		newCollectPath = chooser.getSelectedFile().getAbsolutePath() + File.separator;
+
+		// Must NOT change the available collections to base this collection on here.
+		// It will change any collection the user previously chose to base the newcoll on.
+
+		// will be used to test for duplicate collection names in current collect dir
+		collectDirChanged = newCollectPath.equals(oldCollectPath) ? false : true;
+	    }
+	}
+    }
+
+    private class CancelListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    cancelled = true;
+	    self.dispose();
+	}
+    }
+
+    
+    private class CreateListener
+	implements ActionListener {
+	
+	public void actionPerformed(ActionEvent event) {
+	    // Validate.
+	    title_final = title.getText();
+	    if(title_final.length() == 0) {
+		JOptionPane jOptionPane=new JOptionPane();
+                jOptionPane.setComponentOrientation(Dictionary.getOrientation());
+		jOptionPane.setOpaque(!Utility.isMac());
+		jOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("NewCollectionPrompt.Title_Error"), Dictionary.get("NewCollectionPrompt.Error"), JOptionPane.ERROR_MESSAGE);
+		return;
+	    }
+	    // We must ensure that the collection title is unique. This is a pain in the nether regions as we are forced to load the collect.cfg of each other collection in turn looking for a conflicting title
+	    else {
+		String collectDir = collectDirChanged ? newCollectPath : Gatherer.getCollectDirectoryPath();
+		if(titleClashes(collectDir, title_final, null)) {
+		    JOptionPane jOptionPane=new JOptionPane();
+                    jOptionPane.setComponentOrientation(Dictionary.getOrientation());
+		    jOptionPane.setOpaque(!Utility.isMac());
+		    if (jOptionPane.showConfirmDialog(Gatherer.g_man, Dictionary.get("NewCollectionPrompt.Title_Clash"), Dictionary.get("General.Warning"), JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION) {
+			return;
+		    }
+		}
+	    }		
+	    description_final = description.getText();
+
+	    // If we got this far there are no errors.
+	    Item item_final = (Item) base_collection.getSelectedItem();
+	    base_final = item_final.getFile();
+	    
+	    cancelled = false;
+		
+		self.dispose();
+	    
+	    // going through with collection creation
+	    if(collectDirChanged) {		
+		Gatherer.collectDirectoryHasChanged(Gatherer.getCollectDirectoryPath(), 
+				newCollectPath, Gatherer.g_man.getContentPane()); 
+		// will tell the server that the collect directory has changed and that
+		// the workspace needs to be refreshed (Documents in Greenstone Collections)
+	    }
+	}
+    }
+    
+    private class DescriptionListener
+	extends KeyAdapter {
+	public void keyPressed(KeyEvent event) {
+	    if(event.getKeyCode() == KeyEvent.VK_TAB) {
+		event.consume();
+		base_collection.grabFocus();
+	    }
+	}
+    }
+
+    private class Item
+	implements Comparable {
+	private BasicCollectionConfiguration config;
+	private File file;
+	private String name;
+	public Item(File file, BasicCollectionConfiguration config) {
+	    this.config = config;
+	    this.file = file;
+	    this.name = null;
+	}
+	public Item(File file, String name) {
+	    this.config = null;
+	    this.file = file;
+	    this.name = name;
+	}
+	public int compareTo(Object other) {
+	    if(this == null && other == null) {
+		return 0;
+	    }
+	    else if(other == null) { 
+		return 1; 
+	    }
+	    else if(this == null) {
+		return -1;
+	    } 
+	    else {
+		return toString().toLowerCase().compareTo(other.toString().toLowerCase());
+	    }
+	}
+	public boolean equals(Object other) {
+	    return compareTo(other) == 0;
+	}
+	public File getFile() {
+	    return file;
+	}
+	public String toString() {
+	    if(name == null && config != null) {
+		name = config.toString();
+	    }
+	    return name;
+	}
+    }
+
+    /** FileFilter for the Filechooser to choose the collection to base the new one on: 
+     * Only displays directories. */
+    private class CollectionFileFilter extends FileFilter {	
+	public boolean accept(File f) {
+	    return f.isDirectory();
+	}
+	public String getDescription() {
+	    return "greenstone collection view"; //return "directories only";
+	}
+    }
+
+    /** Folders that can be used as a base collection have a special icon */
+    private class CollectionFileView extends FileView {
+	final String configfile_name;	
+	final ImageIcon COLLECTION_ICON = JarTools.getImage("gsCollection.gif", true);
+	
+	private File previous = null;
+	private boolean previousWasCollection = false;
+
+	public CollectionFileView() {
+	    configfile_name = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+	}
+
+	// A human readable description of the file.
+	public String getDescription(File f) {
+	    if(!f.equals(previous)) {
+		previous = f;
+		// no idea if it's a collection or not, calculate this next time
+		previousWasCollection = false; 
+	    }
+	    return null; // FilewView superclass will handle it
+	}
+        
+	// The icon that represents this file in the JFileChooser.
+	public Icon getIcon(File f) {
+	    
+	    if(f.equals(previous) && previousWasCollection) {
+		return COLLECTION_ICON;
+	    } 
+	    
+	    if(isCollectionBase(f)) {
+		return COLLECTION_ICON;
+	    }	    
+	    
+	    return null; // FileView superclass will do the default
+	}
+        
+	// The name of the file.
+	public String getName(File f) { 
+	    if(!f.equals(previous)) {
+		previous = f;
+		// no idea if it's a collection or not, calculate this next time
+		previousWasCollection = false; 
+	    }
+	    return f.getName(); 
+	}
+
+	// A human readable description of the type of the file.
+	public String getTypeDescription(File f) {
+	    if(f.equals(previous) && previousWasCollection) {
+		return "collection";
+	    } 
+	    if(isCollectionBase(f)) {
+		return "collection";
+	    }
+	    // else
+	    return "not a collection";
+	}
+
+	// Whether the directory is traversable or not.
+	public Boolean isTraversable(File f) {
+	    if(f.equals(previous) && previousWasCollection) {
+		return Boolean.FALSE;
+	    }		
+	    
+	    if(isCollectionBase(f)) {
+		return Boolean.FALSE;
+	    }
+
+	    if(f.isFile()) { // not traversable
+		return Boolean.FALSE;
+	    }
+	    // any other kind of directory is traversable
+	    return Boolean.TRUE;	    
+	}
+
+    
+	/** returns false for collect groups and collect dirs. */
+	private boolean isCollectionBase(File f) {	    
+	    previous = f;
+	    previousWasCollection = false;
+
+	    if(!f.isFile()) {
+		// If the directory has a etc/collect.cfg file then it looks like a collection
+		File collect_cfg_file = new File(f, configfile_name);
+		if (collect_cfg_file.exists()) {
+		    // Check for group coll: Check the collect.cfg for collectgroup=true
+		    BasicCollectionConfiguration config = new BasicCollectionConfiguration(collect_cfg_file);
+		    if (!config.getCollectGroup().equals("true")) {
+			previousWasCollection = true;
+		    }		
+		}
+	    }
+	    return previousWasCollection;
+	}
+
+    }
+}
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NewFolderOrFilePrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NewFolderOrFilePrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NewFolderOrFilePrompt.java	(revision 31635)
@@ -0,0 +1,186 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.file.FileManager;
+
+public class NewFolderOrFilePrompt
+    extends JDialog 
+    implements ActionListener {
+    private CollectionTreeNode node;
+    private JButton cancel_button;
+    private JButton ok_button;
+    private JTextField name_field;
+    private String name;
+
+    private int type;
+    private String extension="";
+    static final private Dimension SIZE = new Dimension(350,115);
+
+    public NewFolderOrFilePrompt(CollectionTreeNode node, int type, String extension) {
+	super(Gatherer.g_man, true);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	if (type == FileManager.FILE_TYPE) {
+	    setTitle(Dictionary.get("NewFolderOrFilePrompt.Title_File"));
+	} else {
+	    setTitle(Dictionary.get("NewFolderOrFilePrompt.Title_Folder"));
+	}
+	this.type = type;
+	if (extension != null) {
+	    this.extension = extension;
+	}
+	this.node = node;
+    }
+
+    public void actionPerformed(ActionEvent event) {
+	if (event.getSource() == ok_button) {
+	    name = name_field.getText()+extension;
+	}
+	else if(event.getSource() == cancel_button) {
+	    name = null;
+	}
+	dispose();
+    }
+
+    public String display() {
+	setSize(SIZE);
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel labels_pane = new JPanel();
+        labels_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel fields_pane = new JPanel();
+        fields_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel info_pane = new JPanel();
+        info_pane.setComponentOrientation(Dictionary.getOrientation());
+
+	JLabel destination_label = new JLabel(Dictionary.get("NewFolderOrFilePrompt.Destination_Name"));
+        destination_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	JTextField destination_textfield = new JTextField(node.getFile().getName());
+        destination_textfield.setComponentOrientation(Dictionary.getOrientation());
+	destination_textfield.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	destination_textfield.setEditable(false);
+	
+	JLabel name_label = new JLabel();
+        name_label.setComponentOrientation(Dictionary.getOrientation());
+	name_field = new JTextField(getAutomaticName());
+        name_field.setComponentOrientation(Dictionary.getOrientation());
+		name_field.selectAll(); // select all the text in the textfield
+		
+	if (type == FileManager.FILE_TYPE) {
+	    name_label.setText(Dictionary.get("NewFolderOrFilePrompt.File_Name"));
+	    name_field.setToolTipText(Dictionary.get("NewFolderOrFilePrompt.File_Name_Tooltip"));
+	} else {
+	    name_label.setText(Dictionary.get("NewFolderOrFilePrompt.Folder_Name"));
+	    name_field.setToolTipText(Dictionary.get("NewFolderOrFilePrompt.Folder_Name_Tooltip"));
+	}
+
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+	
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Pure_Cancel_Tooltip"));
+	
+	// Connection
+	cancel_button.addActionListener(this);
+	ok_button.addActionListener(this);
+	getRootPane().setDefaultButton(ok_button);
+
+	// Layout
+	labels_pane.setLayout(new GridLayout(2,1, 5,0));
+	labels_pane.add(destination_label);
+	labels_pane.add(name_label);
+
+	fields_pane.setLayout(new GridLayout(2,1,0,5));
+	fields_pane.add(destination_textfield);
+	fields_pane.add(name_field);
+	
+	info_pane.setLayout(new BorderLayout(5,0));
+	info_pane.add(labels_pane, BorderLayout.LINE_START);
+	info_pane.add(fields_pane, BorderLayout.CENTER);
+
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	button_pane.setLayout(new GridLayout(1,2,0,5));
+	button_pane.add(ok_button);
+	button_pane.add(cancel_button);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(info_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	// Display
+	Rectangle frame_bounds = Gatherer.g_man.getBounds();
+	setLocation(frame_bounds.x + (frame_bounds.width - SIZE.width) / 2, frame_bounds.y + (frame_bounds.height - SIZE.height) / 2);
+	setVisible(true);
+	return name;
+    }
+
+    private String getAutomaticName()
+    {
+	File file = node.getFile();
+	String default_name;
+	if (type == FileManager.FILE_TYPE) {
+	    default_name = Dictionary.get("NewFolderOrFilePrompt.Default_File_Name");
+	} else {
+	    default_name = Dictionary.get("NewFolderOrFilePrompt.Default_Folder_Name");
+	}
+	
+	File temp_file = new File(file, default_name+extension);
+	int count = 1;
+	while (temp_file.exists()) {
+	    temp_file = new File(file, default_name + " " + count+extension);
+	    count++;
+	}
+	if (extension.equals("")) {
+	    return temp_file.getName();
+	} 
+	String name = temp_file.getName();
+	// remove the extension
+	return name.substring(0, name.length()-extension.length());
+	
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NonWhitespaceField.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NonWhitespaceField.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NonWhitespaceField.java	(revision 31635)
@@ -0,0 +1,77 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+/**************************************************************************************
+ * Written:     04/07/03
+ * Revised:     
+ **************************************************************************************/
+import javax.swing.*;
+import javax.swing.text.*;
+import org.greenstone.gatherer.Dictionary;
+
+/** A JTextField that doesn't allow whitespace characters. 
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.4
+ */
+public class NonWhitespaceField 
+    extends JTextField {
+ 
+    public NonWhitespaceField() {
+	super();
+        this.setComponentOrientation(Dictionary.getOrientation());
+    }
+
+    public NonWhitespaceField(String text) {
+	super(text);
+        this.setComponentOrientation(Dictionary.getOrientation());
+    }
+ 
+    protected Document createDefaultModel() {
+	return new NonWhitespaceDocument();        
+    }
+ 
+    static class NonWhitespaceDocument 
+	extends PlainDocument {
+		  
+	public void insertString(int offs, String str, AttributeSet a) 
+	    throws BadLocationException {
+				
+	    if (str == null) {
+		return;
+	    }
+	    char[] raw = str.toCharArray();
+	    StringBuffer result = new StringBuffer("");
+	    for (int i = 0; i < raw.length; i++) {
+		if(!Character.isWhitespace(raw[i])) {
+		    result.append(raw[i]);
+		}
+	    }
+	    super.insertString(offs, result.toString(), a);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NumberedJTextArea.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NumberedJTextArea.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/NumberedJTextArea.java	(revision 31635)
@@ -0,0 +1,237 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.gui;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+
+import org.fife.ui.rsyntaxtextarea.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.undo.*;
+
+/**
+ * A textarea with the line number next to each line of the text.
+ * It provides undo and redo buttons that are already hooked up to listeners
+ * You can add these buttons to a JComponent and they will behave just like the 
+ * existing RSyntaxArea's Ctrl-Z and Ctrl-Y shortcuts and undo/redo popup menus.
+ */
+public class NumberedJTextArea extends RSyntaxTextArea /* JTextArea */
+    implements UndoableEditListener, ActionListener
+{
+    
+    String id = null;
+    
+    public final GLIButton undoButton;
+    public final GLIButton redoButton;
+
+    public NumberedJTextArea(String tooltip) {
+	this("", tooltip);
+    }
+    
+    public NumberedJTextArea (String id, String tooltip) {
+	super();
+	
+	this.id = id;
+	
+	// maybe use this textarea's id field to customise the undo/redo button tooltips?
+	undoButton = new GLIButton(Dictionary.get("General.Undo"), Dictionary.get("General.Undo_Tooltip"));
+	undoButton.setEnabled(false);
+	
+	redoButton = new GLIButton(Dictionary.get("General.Redo"), Dictionary.get("General.Redo_Tooltip"));
+	redoButton.setEnabled(false);
+	
+	undoButton.addActionListener(this);
+	redoButton.addActionListener(this);
+	
+	// next, initialise this RSyntaxTextArea:
+	
+	// Adding the UndoableEditListener has to come after instantiation of the undo and 
+	// redo buttons, since the listener expects these buttons to already exist
+	this.getDocument().addUndoableEditListener(this);
+	
+	/* Fields specific to RSyntaxQuery inherited class */
+	setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_XML);
+	setBracketMatchingEnabled(true);
+	setAnimateBracketMatching(true);
+	setAntiAliasingEnabled(true);
+	setAutoIndentEnabled(true);
+	setPaintMarkOccurrencesBorder(false);
+	
+	/* Standard fields to JTextArea */
+	setOpaque(false);
+	setBackground(Configuration.getColor("coloring.editable_background", false));
+	setCaretPosition(0);
+	setLineWrap(true);
+	setRows(11);
+	setWrapStyleWord(false);	
+	setToolTipText(tooltip);
+    }
+    
+    public void paintComponent(Graphics g)
+    {
+	Insets insets = getInsets();
+	Rectangle rectangle = g.getClipBounds();
+	g.setColor(Color.white);
+	g.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
+	
+	super.paintComponent(g);
+	
+	if (rectangle.x < insets.left)
+	    {
+		FontMetrics font_metrics = g.getFontMetrics();
+		int font_height = font_metrics.getHeight();
+		int y = font_metrics.getAscent() + insets.top;
+		int line_number_start_point = ((rectangle.y + insets.top) / font_height) + 1;
+		if (y < rectangle.y)
+		    {
+			y = line_number_start_point * font_height - (font_height - font_metrics.getAscent());
+		    }
+		int y_axis_end_point = y + rectangle.height + font_height;
+		int x_axis_start_point = insets.left;
+		x_axis_start_point -= getFontMetrics(getFont()).stringWidth(Math.max(getRows(), getLineCount() + 1) + " ");
+		if (!this.getText().trim().equals(""))
+		    {
+			g.setColor(Color.DARK_GRAY);
+		    }
+		else
+		    {
+			g.setColor(Color.white);
+		    }
+		int length = ("" + Math.max(getRows(), getLineCount() + 1)).length();
+		while (y < y_axis_end_point)
+		    {
+			g.drawString(line_number_start_point + "  ", x_axis_start_point, y);
+			y += font_height;
+			line_number_start_point++;
+		    }
+	    }
+    }
+    
+    
+    public Insets getInsets()
+    {
+	Insets insets = super.getInsets(new Insets(0, 0, 0, 0));
+	insets.left += getFontMetrics(getFont()).stringWidth(Math.max(getRows(), getLineCount() + 1) + " ");
+	return insets;
+    }
+    
+    
+    // Overriding, to ensure that even if ctrl-z was pressed to undo something,
+    // the undo and redo buttons are in sync with that.
+    public void undoLastAction() {
+	super.undoLastAction();
+	redoButton.setEnabled(true);
+	
+	if (!this.canUndo()) {
+	    undoButton.setEnabled(false);
+	} else {
+	    undoButton.setEnabled(true);
+	}
+    }
+    
+    // Overriding
+    public void redoLastAction() {
+	super.redoLastAction();
+	undoButton.setEnabled(true);
+	    
+	if (!this.canRedo()) {
+	    redoButton.setEnabled(false);
+	} else {
+	    redoButton.setEnabled(true);
+	}
+    }
+
+    // Overriding
+    public void discardAllEdits() {
+	// Buttons have to be disabled before discardAllEdits(). If done in reverse order, buttons get re-enabled
+	undoButton.setEnabled(false);
+	redoButton.setEnabled(false);
+	super.discardAllEdits();
+    }
+
+    // The RSyntaxTextarea class already provides undo and redo functionality hooked up to their
+    // usual keyboard shortcuts and to rightclick popup menus. The actionListener below merely
+    // reuses this behaviour and attaches it to the undoButton and redoButton.
+    public void actionPerformed(ActionEvent event)
+    {
+	// actionPerformed() not defined in any of the superclasses RTextArea, RTextAreaBase, JTextArea
+	//super.actionPerformed(event); // compile error
+	
+	if(event.getSource() == undoButton) {
+	    
+	    try {
+		// calls canUndo() and undoLastAction() defined by RTextArea
+		// internally calls its undoManager's canUndo() and undo() to handle compoundEdits
+		if (this.canUndo()) {
+		    this.undoLastAction();					
+		}
+		
+		if (!this.canUndo()) {
+		    undoButton.setEnabled(false);
+		} else {
+		    undoButton.setEnabled(true);
+		}
+		
+	    } catch (Exception e) {
+		System.err.println("Exception trying to undo: " + e.getMessage());
+		e.printStackTrace();
+	    }
+	}
+	else if (event.getSource() == redoButton) {
+	    try {
+		// calls canRedo() and redoLastAction() defined by RTextArea
+		// internally calls its undoManager's canRedo() and redo() to handle compoundEdits
+		
+		if (this.canRedo()) {
+		    this.redoLastAction();
+		}
+		
+		if (!this.canRedo()) {
+		    redoButton.setEnabled(false);
+		} else {
+		    redoButton.setEnabled(true);
+		}
+		
+	    } catch (Exception e) {
+		System.err.println("Exception trying to redo: " + e.getMessage());
+		e.printStackTrace();
+	    }
+	}
+    }
+
+    
+    // defined in interface UndoableEditListener
+    public void undoableEditHappened(UndoableEditEvent evt)
+    {	    
+	undoButton.setEnabled(true);
+	redoButton.setEnabled(false);	    
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/OpenCollectionDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/OpenCollectionDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/OpenCollectionDialog.java	(revision 31635)
@@ -0,0 +1,448 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.BasicCollectionConfiguration;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.shell.GShellEvent;
+import org.greenstone.gatherer.shell.GShellListener;
+import org.greenstone.gatherer.shell.GShellProgressMonitor;
+
+
+/** A dialog which provides a straight-forward access to the currently installed collections. It also will contain the ability to continue through to the original OpenCollectionDialog if your source collection is located somewhere other than the gsdl collect folder. */
+public class OpenCollectionDialog
+    extends ModalDialog {
+
+    static final public int OK_OPTION = 0;
+    static final public int CANCEL_OPTION = 1;
+
+    static final private Dimension SIZE = new Dimension(640,480);
+    static final private String BLANK = "b";
+    static final private String DESCRIPTION = "d";
+
+    private CardLayout card_layout;
+    private int result;
+    private JButton cancel_button;
+    private JButton chdir_button;
+    private JButton open_button;
+    private JList collection_list;
+    private JTextArea description_textarea;
+    private JPanel description_pane;
+    private String filename;
+    private String newCollectPath;
+
+    public OpenCollectionDialog() {
+	super(Gatherer.g_man, "", true);
+	setJMenuBar(new SimpleMenuBar("openingacollection"));
+        this.setComponentOrientation(Dictionary.getOrientation());
+	setSize(SIZE);
+	setTitle(Dictionary.get("OpenCollectionDialog.Title"));
+	newCollectPath = Gatherer.getCollectDirectoryPath();
+
+	// Creation
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel center_pane = new JPanel();
+        center_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel collection_list_pane = new JPanel();
+        collection_list_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel collection_list_label = new JLabel(Dictionary.get("OpenCollectionDialog.Available_Collections"));
+        collection_list_label.setComponentOrientation(Dictionary.getOrientation());
+	collection_list = new JList(new CollectionListModel());
+        collection_list.setComponentOrientation(Dictionary.getOrientation());
+        
+	description_pane = new JPanel();
+        description_pane.setComponentOrientation(Dictionary.getOrientation());
+	card_layout = new CardLayout();
+        
+	JPanel blank_pane = new JPanel();
+        blank_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel description_textarea_pane = new JPanel();
+        description_textarea_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel description_textarea_label = new JLabel(Dictionary.get("OpenCollectionDialog.Description"));
+        description_textarea_label.setComponentOrientation(Dictionary.getOrientation());
+	description_textarea = new JTextArea();
+        description_textarea.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	open_button = new GLIButton(Dictionary.get("OpenCollectionDialog.Open"), Dictionary.get("OpenCollectionDialog.Open_Tooltip"));
+	open_button.setEnabled(false);
+	chdir_button = new GLIButton(Dictionary.get("General.CD"), Dictionary.get("General.CD_Tooltip"));
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Pure_Cancel_Tooltip"));
+	
+	// Connection
+	chdir_button.addActionListener(new ChangeDirListener());
+	if(Gatherer.isGsdlRemote) { // disable changing directories for client GLI
+	    chdir_button.setEnabled(false);
+	}
+	cancel_button.addActionListener(new CancelListener());
+	open_button.addActionListener(new OpenListener());
+	CollectionListSelectionListener clsl = new CollectionListSelectionListener();
+	collection_list.addListSelectionListener(clsl);
+	collection_list.addMouseListener(clsl);
+	clsl = null;
+
+	// Layout
+        JScrollPane scrol_tmp;
+        
+	collection_list_pane.setLayout(new BorderLayout());
+	collection_list_pane.add(collection_list_label, BorderLayout.NORTH);
+        scrol_tmp = new JScrollPane(collection_list);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	collection_list_pane.add(scrol_tmp, BorderLayout.CENTER);
+
+	description_textarea_pane.setLayout(new BorderLayout());
+	description_textarea_pane.add(description_textarea_label, BorderLayout.NORTH);
+	scrol_tmp =new JScrollPane(description_textarea);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+        description_textarea_pane.add(scrol_tmp, BorderLayout.CENTER);
+
+	description_pane.setLayout(card_layout);
+	description_pane.add(description_textarea_pane, DESCRIPTION);
+	description_pane.add(blank_pane, BLANK);
+
+	center_pane.setLayout(new GridLayout(2,1,0,5));
+	center_pane.add(collection_list_pane);
+	center_pane.add(description_pane);
+
+	button_pane.setLayout(new GridLayout(1,3));
+	button_pane.add(open_button);
+	button_pane.add(chdir_button);
+	button_pane.add(cancel_button);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(center_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+	// Final dialog setup & positioning.
+	getRootPane().setDefaultButton(open_button);
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	screen_size = null;
+    }
+
+    public void destroy() {
+    }
+
+    public int display() {
+	setVisible(true);
+	return result;
+    }
+
+    public String getFileName() {
+	return this.filename;
+    }
+
+    private class ChangeDirListener implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    JFileChooser chooser = new JFileChooser(newCollectPath);
+	    chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+	    chooser.setDialogTitle(Dictionary.get("General.ChooseCollectDirectory"));
+	    int returnVal = chooser.showOpenDialog(OpenCollectionDialog.this);
+	    if(returnVal == JFileChooser.APPROVE_OPTION) {
+		String oldCollectPath = newCollectPath;
+		newCollectPath = chooser.getSelectedFile().getAbsolutePath() + File.separator;
+		
+		if(!newCollectPath.equals(oldCollectPath)) {
+		    // reload the list of available collections using this new path
+		    CollectionListModel listModel = (CollectionListModel)collection_list.getModel();
+		    listModel.reload(newCollectPath);
+		    collection_list.repaint();
+		}
+	    } 
+	}
+    }
+
+    private class CancelListener
+	implements ActionListener {
+
+	public void actionPerformed(ActionEvent event) {
+	    result = CANCEL_OPTION;
+	    OpenCollectionDialog.this.dispose();
+	}
+    }
+
+    private class CollectionListSelectionListener
+	extends MouseAdapter
+	implements ListSelectionListener {
+
+	public void mouseClicked(MouseEvent event) {
+	    ///ystem.err.println("Mouse clicked");
+	    if(event.getClickCount() >= 2) {
+		Point location = event.getPoint();
+		int index = collection_list.locationToIndex(location);
+		collection_list.setSelectedIndex(index);
+		location = null;
+		open_button.doClick();
+	    }
+	}
+
+	public void valueChanged(ListSelectionEvent event) {
+	    if(collection_list.isSelectionEmpty()) {
+		card_layout.show(description_pane, BLANK);
+		open_button.setEnabled(false);
+	    }
+	    else {
+		BasicCollectionConfiguration collection_configuration = (BasicCollectionConfiguration) collection_list.getSelectedValue();
+		description_textarea.setText(collection_configuration.getDescription());
+		description_textarea.setCaretPosition(0);
+		card_layout.show(description_pane, DESCRIPTION);
+		open_button.setEnabled(true);
+	    }
+	}
+    }
+
+    // use this if we ever go back to viewing all colls at once
+    /*    private class GS3CollectionListModel
+	extends AbstractListModel {
+
+	private TreeSet data;
+
+	public GS3CollectionListModel() {
+	    data = new TreeSet();
+	    File sites_folder = new File(Utility.getSitesDir(Configuration.gsdl3_path));
+	    File sites[] = sites_folder.listFiles();
+	    for (int s=0; s<sites.length; s++) {
+		String site_name = sites[s].getName();
+		File collect_folder = new File(Utility.getCollectDir(Configuration.gsdl3_path, site_name) );
+		if (!collect_folder.exists()) {
+		    continue;
+		}
+		File[] collection_folders = collect_folder.listFiles();
+		for(int i = 0; i < collection_folders.length; i++) {
+		    File collection_folder = collection_folders[i];
+		    String collection_foldername = collection_folder.getName();
+		    if(!collection_folder.isFile() && !collection_foldername.equals(StaticStrings.MODEL_COLLECTION_NAME)) {
+			BasicCollectionConfiguration collection_configuration = new BasicCollectionConfiguration(new File(collection_folder, Utility.CONFIG_FILE), site_name);
+			if(!collection_configuration.getName().equals(StaticStrings.ERROR_STR)) {
+			    data.add(collection_configuration);
+			}
+		    }
+		    collection_foldername = null;
+		    collection_folder = null;
+		} // for each collection
+		collection_folders = null;
+		collect_folder = null;
+	    } // for each site
+	    sites_folder = null;
+	    sites = null;
+	}
+
+	public Object getElementAt(int index) {
+	    Iterator iterator = data.iterator();
+	    for(int i = 0; iterator.hasNext(); i++) {
+		Object object = iterator.next();
+		if(i == index) {
+		    iterator = null;
+		    return object;
+		}
+		object = null;
+	    }
+	    iterator = null;
+	    return null;
+	}
+
+	public int getSize() {
+	    return data.size();
+	}
+    }
+    */	
+    private class CollectionListModel
+	extends AbstractListModel {
+
+	private TreeSet data;
+	static final public int COLLECT   = 3;
+
+	public CollectionListModel() {
+	    data = new TreeSet();
+	    reload(Gatherer.getCollectDirectoryPath());
+	}
+
+	// to be called when the collection directory has changed
+	public void reload(String collectDirectoryPath) {
+	    data.clear();
+	    File collect_directory = new File(collectDirectoryPath);
+	    if (!collect_directory.exists()) {
+		collect_directory = null;
+		return;
+	    }
+
+	    load_collection_configs(data,collect_directory);
+
+	    // list size/contents has changed, this listmodel must notify the 
+	    // encapsulating JList, else it won't be able to display longer lists
+	    fireContentsChanged(this, 0, data.size()); 
+	}
+
+
+	protected void load_collection_configs(TreeSet data, File collect_directory) {
+
+	    File[] collection_folders = collect_directory.listFiles();
+
+	    for(int i = 0; i < collection_folders.length; i++) {
+		File collection_folder = collection_folders[i];
+		String collection_foldername = collection_folder.getName();
+
+		if(collection_folder.isDirectory() && !collection_foldername.equals(StaticStrings.MODEL_COLLECTION_NAME)) {
+		    // this 'file_name' has already been prefixed by 'etc'
+		    String file_name;
+		    if (Gatherer.GS3){
+			convertToGS3Collection(collection_folder);
+			file_name = Utility.CONFIG_GS3_FILE;
+		    }else{
+			file_name = Utility.CONFIG_FILE;
+		    }
+		    
+		    File config_file = new File(collection_folder, file_name);
+		    if (config_file.exists()) {
+			BasicCollectionConfiguration collection_configuration = new BasicCollectionConfiguration(config_file);
+
+			// look to see if group set?
+			// => if it is, don't add, but recurse to look for child collections in collect-group
+
+			if (collection_configuration.getCollectGroup().equals("true")) {
+			    load_collection_configs(data, collection_folder);
+			}
+			else {
+			  // add this collection. We want to allow non gli collections, so add anything that has a config file.
+			  data.add(collection_configuration);
+			}
+		    }
+		    config_file = null;
+		}
+		collection_foldername = null;
+		collection_folder = null;
+	    }
+	    collection_folders = null;
+	    collect_directory = null;
+	}
+
+
+
+	public Object getElementAt(int index) {
+	    Iterator iterator = data.iterator();
+	    for(int i = 0; iterator.hasNext(); i++) {
+		Object object = iterator.next();
+		if(i == index) {
+		    iterator = null;
+		    return object;
+		}
+		object = null;
+	    }
+	    iterator = null;
+	    return null;
+	}
+
+	public int getSize() {
+	    return data.size();
+	}
+
+	public void convertToGS3Collection(File collection_folder) {
+
+	    File collect_cfg_file = new File(collection_folder.getAbsolutePath() + File.separator + "etc" + File.separator + "collect.cfg");
+	    File build_cfg_file = new File(collection_folder.getAbsolutePath() + File.separator+"index" + File.separator + "build.cfg");
+
+	    if (collect_cfg_file.exists() && build_cfg_file.exists()){
+		// Generate the convert_coll_from_gs2.pl command
+		ArrayList command_parts_list = new ArrayList();
+		if (!Gatherer.isGsdlRemote) {
+		    command_parts_list.add(Configuration.perl_path);
+		    command_parts_list.add("-S");
+		}
+		command_parts_list.add(Configuration.getGS3ScriptPath() + "convert_coll_from_gs2.pl");
+		command_parts_list.add("-collectdir");
+		command_parts_list.add(collection_folder.getParent());
+		command_parts_list.add(collection_folder.getName());
+
+		// Run the convert_coll_from_gs2.pl command
+		String[] command_parts = (String[]) command_parts_list.toArray(new String[0]);
+		GShell process = new GShell(command_parts, GShell.CONVERT, COLLECT, null, null, GShell.GSHELL_CONVERT);
+		//process.addGShellListener(this);
+		process.run(); // Don't bother threading this... yet
+		
+		// From now, won't dangerously delete GS2 collect and build config files anymore
+		//collect_cfg_file.delete();
+		//build_cfg_file.delete();
+
+		File collect_bak_file = new File(collection_folder.getAbsolutePath() + File.separator + "etc" + File.separator + "collect.cfg.bak");
+		File build_bak_file = new File(collection_folder.getAbsolutePath() + File.separator+"index" + File.separator + "build.cfg.bak");
+		if(!collect_cfg_file.renameTo(collect_bak_file)) {
+		    System.err.println("Unable to move collect.cfg to " + collect_bak_file);
+		}
+		if(!build_cfg_file.renameTo(build_bak_file)) {
+		    System.err.println("Unable to move build.cfg to " + build_bak_file);
+		}
+	    }
+	}
+    }
+
+    private class OpenListener
+	implements ActionListener {
+
+	public void actionPerformed(ActionEvent event) {
+	    result = OK_OPTION;
+	    Object selected_object = collection_list.getSelectedValue();
+	    if (selected_object instanceof BasicCollectionConfiguration) {
+		BasicCollectionConfiguration collection_configuration = (BasicCollectionConfiguration)selected_object; //(BasicCollectionConfiguration) collection_list.getSelectedValue();
+		//****************
+		File collect_cfg_file = collection_configuration.getFile(); // returns the collect.cfg file
+		result = FormatConversionDialog.checkForGS2FormatStatements(collect_cfg_file);
+		if(result == OK_OPTION) { // either there were no gs2 format stmts or user chose to proceed
+		    File etc_folder = collect_cfg_file.getParentFile();
+		    File collection_folder = etc_folder.getParentFile();
+		    filename = collection_folder.getAbsolutePath() + File.separator + "gli.col";
+		    collection_folder = null;
+		    etc_folder = null;
+		    collect_cfg_file = null;
+		    collection_configuration = null;
+		}
+		OpenCollectionDialog.this.dispose();
+	    }
+
+	    Gatherer.collectDirectoryHasChanged(Gatherer.getCollectDirectoryPath(), 
+				newCollectPath, Gatherer.g_man.getContentPane()); 
+	        // will tell the server that the collect directory has changed and that
+			// the workspace needs to be refreshed (Documents in Greenstone Collections)
+
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/OptionsPane.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/OptionsPane.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/OptionsPane.java	(revision 31635)
@@ -0,0 +1,646 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.Argument;
+import org.greenstone.gatherer.cdm.ArgumentControl;
+import org.greenstone.gatherer.collection.ScriptOptions;
+import org.greenstone.gatherer.collection.Collection;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.util.AppendLineOnlyFileDocument;
+import org.greenstone.gatherer.util.AppendLineOnlyFileDocumentOwner;
+import org.greenstone.gatherer.util.StaticStrings;
+
+/** This class serves as the data holder for all subclasses of option panes, such as Import options or All options. It also contains methods for creating each of the option lines as they would appear in the subpane. Futhermore it has a method for considering all the arguments and generating a <strong>String[]</strong> to allow you to pass them to the <strong>GShell</strong>.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.2
+ */
+public class OptionsPane
+    extends JPanel 
+    implements AppendLineOnlyFileDocumentOwner, MouseListener {
+
+    static final public char SUCCESSFUL = 's';
+    static final public char UNSUCCESSFUL = 'u';
+    static final public char CANCELLED = 'c';
+    static final public char UNKNOWN = 'x';
+    static final public char SCHEDULED = 'd'; 
+
+    static private int BUILD = 0;
+    static private int IMPORT = 1;
+    static private int SCHEDULE = 2; 
+    static private int MINIMUM_ROWS = 15;
+    
+    /** All process messages are written to this log text area. */
+    public JTextArea log_textarea = null;
+
+    private ArrayList current_controls;
+
+    /** The <strong>ScriptOptions</strong> data object contains all the option settings we wish to persist between Gatherer sessions (and thus is stored in <strong>Collection</strong>). */
+    private ScriptOptions build_options = null;
+    private ScriptOptions import_options = null;
+    private ScriptOptions schedule_options = null;
+
+    private FileEntry file_entry = null;
+
+    /** the log pane - we only create it once now, not each time */
+    private JPanel log_pane = null;
+    /** the list of previous log messages */
+    private JList log_list = null;
+    private Vector writing_documents;
+
+
+    /** The default constructor creates the few session length options, but either retrieves the rest from the current collection, or creates a default set of options. */
+    public OptionsPane(ScriptOptions import_options, ScriptOptions build_options, ScriptOptions schedule_options) {
+	this.build_options = build_options;
+	this.import_options = import_options;
+	this.schedule_options = schedule_options; 
+	this.current_controls = new ArrayList();
+	this.writing_documents = new Vector();
+        this.setComponentOrientation(Dictionary.getOrientation());
+        
+	// Have to do this here, not in display, as the message log view may not have been displayed yet.
+	log_textarea = new JTextArea();
+        log_textarea.setComponentOrientation(Dictionary.getOrientation());
+	log_textarea.setEditable(false);
+    }
+
+    /** This method creates the panel with all the build only options on it.
+     * @param pane a JPanel which already has previous arguments available on it, to allow for lower moes to concatenate all of the arguments together
+     * @return a JPanel which can be used to display all the build only options
+     * @see org.greenstone.gatherer.Configuration#EXPERT_MODE
+     * @see org.greenstone.gatherer.Configuration#getColor
+     * @see org.greenstone.gatherer.Configuration#getMode
+     * @see org.greenstone.gatherer.Gatherer#config
+     * @see org.greenstone.gatherer.cdm.Argument
+     * @see org.greenstone.gatherer.collection.BuildOptions#getBuildArgument
+     * @see org.greenstone.gatherer.collection.BuildOptions#getBuildArgumentCount
+     * @see org.greenstone.gatherer.collection.BuildOptions#getBuildValue
+     * @see org.greenstone.gatherer.collection.BuildOptions#getBuildValueEnabled
+     * @see org.greenstone.gatherer.gui.OptionsPane.MyArgumentControl
+     */
+    public JPanel buildBuild(JPanel pane) {
+      // Reset the arguments
+      if(pane == null) {
+        current_controls.clear();
+      }
+      ArrayList build_arguments = new ArrayList();
+      int current_mode = Configuration.getMode();
+      int total_build_argument_count = build_options.getArgumentCount();
+      
+      for(int i = 0; i < total_build_argument_count; i++) {
+        // Retrieve the argument so we know how to format the control.
+        Argument argument = build_options.getArgument(i);
+        
+        if(!argument.isHiddenGLI() && argument.getModeLevel() <= current_mode) {
+          // Now attempt to retrieve any existing value for this argument.
+          boolean enabled = build_options.getValueEnabled(argument.getName());
+          String value = build_options.getValue(argument.getName());
+          MyArgumentControl argument_control = new MyArgumentControl(BUILD, argument, enabled, value);
+          build_arguments.add(argument_control);
+        }
+      }
+      current_controls.addAll(build_arguments);
+      
+      // Now that we know how many arguments there are we can build the pane to view them on. Modes lower than EXPERT can provide a previous pane on which to add the arguments.
+      if(pane == null || current_mode >= Configuration.EXPERT_MODE) {
+        pane = new JPanel();
+        pane.setComponentOrientation(Dictionary.getOrientation());
+        pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+        pane.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+        int argument_count = build_arguments.size();
+        // If in any of the higher detail modes, and assuming we don't want super phat argument controls, we better ensure there is a minimum number of lines in the grid layout
+        if(current_mode >= Configuration.EXPERT_MODE) {
+          if(argument_count < MINIMUM_ROWS) {
+            argument_count = MINIMUM_ROWS;
+          }
+          pane.setLayout(new GridLayout(argument_count, 1, 5, 5));
+        }
+        // Otherwise we're just going to throw them on one after another and chuck it in a scroll pane anyway
+        else {
+          // use  GridLayout with 0 rows == as many rows as needed. Unfortunately, rows will
+          // grow fat if space too large, but we don't know how many rows we'll need yet.
+          pane.setLayout(new GridLayout(0, 1, 5, 5));
+        }
+      }
+      
+      for(int j = 0; j < build_arguments.size(); j++) {
+        pane.add((JComponent)build_arguments.get(j));
+      }
+      pane.addMouseListener(this);
+      build_arguments = null;
+      return pane;
+    }
+
+    /**Wendy's attempt at writing a buildSchedule Jpanel. */
+    public JPanel buildSchedule(JPanel pane) {
+	//reset the arguments
+	if(pane ==  null) {
+	    current_controls.clear(); 
+	}
+
+	ArrayList schedule_arguments = new ArrayList(); 
+	int current_mode = Configuration.getMode(); 
+
+	int total_schedule_argument_count = schedule_options.getArgumentCount();
+      
+      for(int i = 0; i < total_schedule_argument_count; i++) {
+        // Retrieve the argument so we know how to format the control.
+        Argument argument = schedule_options.getArgument(i);
+        
+       if(!argument.isHiddenGLI() && argument.getModeLevel() <= current_mode) {
+          // Now attempt to retrieve any existing value for this argument.
+          boolean enabled = schedule_options.getValueEnabled(argument.getName());
+          String value = schedule_options.getValue(argument.getName());
+          MyArgumentControl argument_control = new MyArgumentControl(SCHEDULE, argument, enabled, value);
+          schedule_arguments.add(argument_control);
+        }
+      }
+      current_controls.addAll(schedule_arguments);
+      
+      // Now that we know how many arguments there are we can build the pane to view them on. Modes lower than EXPERT can provide a previous pane on which to add the arguments.
+      if(pane == null || current_mode >= Configuration.EXPERT_MODE) {
+        pane = new JPanel();
+        pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+        pane.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+        int argument_count = schedule_arguments.size();
+        // If in any of the higher detail modes, and assuming we don't want super phat argument controls, we better ensure there is a minimum number of lines in the grid layout
+        if(current_mode >= Configuration.EXPERT_MODE) {
+          if(argument_count < MINIMUM_ROWS) {
+            argument_count = MINIMUM_ROWS;
+          }
+          pane.setLayout(new GridLayout(argument_count, 1, 5, 5));
+        }
+        // Otherwise we're just going to throw them on one after another and chuck it in a scroll pane anyway
+        else {
+          // use  GridLayout with 0 rows == as many rows as needed. Unfortunately, rows will
+          // grow fat if space too large, but we don't know how many rows we'll need yet.
+          pane.setLayout(new GridLayout(0, 1, 5, 5));
+        }
+      }
+      
+      for(int j = 0; j < schedule_arguments.size(); j++) {
+        pane.add((JComponent)schedule_arguments.get(j));
+      }
+      pane.addMouseListener(this);
+      schedule_arguments = null;
+      return pane;
+
+    }
+
+    /** This method creates the panel with all the import only options on it.
+     * @param pane a JPanel which already has previous arguments available on it, to allow for lower moes to concatenate all of the arguments together
+     * @return a JPanel which can be used to display all the build only options
+     * @see org.greenstone.gatherer.Configuration#EXPERT_MODE
+     * @see org.greenstone.gatherer.Configuration#getColor
+     * @see org.greenstone.gatherer.Configuration#getMode
+     * @see org.greenstone.gatherer.Gatherer#config
+     * @see org.greenstone.gatherer.cdm.Argument
+     * @see org.greenstone.gatherer.collection.BuildOptions#getImportArgument
+     * @see org.greenstone.gatherer.collection.BuildOptions#getImportArgumentCount
+     * @see org.greenstone.gatherer.collection.BuildOptions#getImportValue
+     * @see org.greenstone.gatherer.collection.BuildOptions#getImportValueEnabled
+     * @see org.greenstone.gatherer.gui.OptionsPane.ArgumentControl
+     */
+    public JPanel buildImport(JPanel pane) {
+	// Reset the arguments
+	if(pane == null) {
+	    current_controls.clear();
+	}
+	ArrayList import_arguments = new ArrayList();
+	int current_mode = Configuration.getMode();
+	int total_import_argument_count = import_options.getArgumentCount();       
+	for(int i = 0; i < total_import_argument_count; i++) {
+	    // Retrieve the argument so we know how to format the control.
+	    Argument argument = import_options.getArgument(i);
+	    if(!argument.isHiddenGLI() && argument.getModeLevel() <= current_mode) {
+		// Now attempt to retrieve any existing value for this argument.
+		boolean enabled = import_options.getValueEnabled(argument.getName());
+		String value = import_options.getValue(argument.getName());
+		MyArgumentControl argument_control = new MyArgumentControl(IMPORT, argument, enabled, value);
+		import_arguments.add(argument_control);
+	    }
+	}
+	current_controls.addAll(import_arguments);
+	// Now that we know how many arguments there are we can build the pane to view them on. Modes lower than EXPERT can provide a previous pane on which to add the arguments.
+	if(pane == null || current_mode >= Configuration.EXPERT_MODE) {
+	    pane = new JPanel();
+            pane.setComponentOrientation(Dictionary.getOrientation());
+	    pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    pane.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    int argument_count = import_arguments.size();	    
+	    // If in any of the higher detail modes, and assuming we don't want super phat argument controls, we better ensure there is a minimum number of lines in the grid layout
+	    if(current_mode >= Configuration.EXPERT_MODE) {
+		if(argument_count < MINIMUM_ROWS) {
+		    argument_count = MINIMUM_ROWS;
+		}
+		pane.setLayout(new GridLayout(argument_count, 1, 5, 5));
+	    }
+	    // Otherwise we're just going to throw them on one after another and chuck it in a scroll pane anyway
+	    else {
+		// use  GridLayout with 0 rows == as many rows as needed. Unfortunately, rows will 
+		// grow fat if space too large, but we don't know how many rows we'll need yet.
+		pane.setLayout(new GridLayout(0, 1, 5, 5));
+	    }
+	} 
+
+	for(int j = 0; j < import_arguments.size(); j++) {
+	    pane.add((JComponent)import_arguments.get(j));
+	}
+	pane.addMouseListener(this);
+	import_arguments = null;
+	return pane;
+    }
+
+    /** This method is used to build a panel based on the message log, which is nothing like any of the other panels.
+     * @return A <strong>JPanel</strong> containing a scrollable text area which represents the shell process message log.
+     */
+    public JPanel buildLog() {
+	// we now save the log pane
+	if (log_pane == null) {
+	    log_pane = new JPanel(new BorderLayout());
+            log_pane.setComponentOrientation(Dictionary.getOrientation());
+	    // Build a list of the log files available, ordering by last modified. Log files are like build_log.date.txt
+	    DefaultListModel contents = new DefaultListModel();
+	    File log_directory = new File(CollectionManager.getLoadedCollectionLogDirectoryPath());
+	    File children[] = log_directory.listFiles();
+	    for(int i = 0; children != null && i < children.length; i++) {
+		String filename = children[i].getName();
+		if(filename.startsWith("build_log.") && filename.endsWith(".txt") ) {
+		    String datestamp = filename.substring(filename.indexOf(".") + 1, filename.lastIndexOf(".")).toLowerCase();
+		    if(datestamp.indexOf("s") == -1 && datestamp.indexOf("u") == -1 && datestamp.indexOf("c") == -1 && datestamp.indexOf("x") == -1) {
+			FileEntry entry = new FileEntry(children[i].getName(), children[i].getAbsolutePath());
+			// We are about to insert it. But where.
+			boolean found = false;
+			for(int j = 0; !found && j < contents.size(); j++) {
+			    FileEntry sibling = (FileEntry) contents.getElementAt(j);
+			    int order = entry.compareTo(sibling);
+			    if(order > 0) {
+				contents.insertElementAt(entry, j);
+				found = true;
+			    }
+			}
+			if(!found) {
+			    contents.addElement(entry);
+			}
+		    }
+		}
+	    }
+
+	    log_list = new JList(contents);
+            log_list.setComponentOrientation(Dictionary.getOrientation());
+	    log_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+	    log_list.setLayoutOrientation(JList.VERTICAL);
+	    log_list.setPreferredSize(new Dimension(600, 100));
+	    log_list.setVisibleRowCount(3);
+	    log_list.addListSelectionListener(new LogListListener());
+
+	    JLabel log_history_label = new JLabel(Dictionary.get("OptionsPane.LogHistory"));
+	    log_history_label.setComponentOrientation(Dictionary.getOrientation());
+            JPanel log_history_pane = new JPanel();
+            log_history_pane.setComponentOrientation(Dictionary.getOrientation());
+	    log_history_pane.setPreferredSize(new Dimension(600, 100));
+	    log_history_pane.setLayout(new BorderLayout());
+	    log_history_pane.add(log_history_label, BorderLayout.NORTH);
+            JScrollPane scrol_tmp=new JScrollPane(log_list);
+            scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	    log_history_pane.add(scrol_tmp, BorderLayout.CENTER);
+	    
+            scrol_tmp=new JScrollPane(log_textarea);
+            scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	    log_pane.add(scrol_tmp, BorderLayout.CENTER);
+	    log_pane.add(log_history_pane, BorderLayout.SOUTH);
+	}
+	return log_pane;
+    }
+
+    public AppendLineOnlyFileDocument createNewLogDocument() {
+	long time = System.currentTimeMillis();
+	StringBuffer name = new StringBuffer();
+	name.append("build_log.");
+	name.append(time);
+	name.append(".txt");
+	// just in case there is no log directory
+	File file = new File(CollectionManager.getLoadedCollectionLogDirectoryPath() + name.toString());
+	File parent_file = file.getParentFile();
+	parent_file.mkdirs();
+	parent_file = null;
+	// create the file entry and add it to the list at pos 0 - it will always be the newest one created
+	file_entry = new FileEntry(name.toString(), file.getAbsolutePath());
+	((DefaultListModel)log_list.getModel()).add(0, file_entry);
+	log_list.setSelectedIndex(0);
+	// Finally retrieve and return the document associated with this file entry
+	return file_entry.getDocument();
+    }
+	
+	/** ensure the current build_log file has finished transferring from memory to disk, and that the random access
+	*  file is properly closed when a collection is closed.  Else we can't delete the just-closed collection since the 
+	* build_log file resource is still kept open. */
+	public void closeCurrentLogDocument() {
+		if(file_entry == null) {
+			return;
+		}
+		AppendLineOnlyFileDocument buildDoc = file_entry.getDocument();
+		if(buildDoc != null) {
+			buildDoc.close();
+		}		
+	}
+
+
+    /** Attempts to discover the latest document count.
+     * @return An <strong>int</strong> detailing the number of documents in this collection.
+     */
+    public int getDocumentCount() {
+	if(Gatherer.c_man.ready()) {
+	    int count = Gatherer.c_man.getCollection().getDocumentCount();
+	    if(count != 0) {
+		return count;
+	    }
+	}
+	return 1;
+    }
+
+    /** Called by our magic log documents after they have finished writing themselves to file, whereapon it is no longer necessary to hold a reference to them. */
+    public void remove(AppendLineOnlyFileDocument document) {
+	writing_documents.remove(document);
+    }
+
+    public void resetFileEntry() {
+	if(file_entry != null) {
+	    file_entry.reset();
+	}
+    }
+
+    /** Given a panel containing ArgumentControls, update the values associated with them. */
+    public void update(JPanel panel) {
+	if(panel == log_pane) {
+	    return;
+	}
+
+	for(int i = 0; i < panel.getComponentCount(); i++) {
+	    Component component = panel.getComponent(i);
+	    if(component instanceof MyArgumentControl) {
+		((MyArgumentControl)component).update();
+	    }
+	}
+    }
+
+    /** Implementation side-effect
+     * @param e a MouseEvent
+     */
+    public void mouseClicked(MouseEvent e) {}
+
+    /** Implementation side-effect
+     * @param e a MouseEvent
+     */
+    public void mouseEntered(MouseEvent e) {}
+
+    /** Implemented to ensure that, by the time the mouse pointer leaves the current build options screen, ang changes to the value the JSpinners have been commited
+     * @param e a MouseEvent
+     */
+    public void mouseExited(MouseEvent e) {
+	// Loop through the controls, and if the current control is a JSpinner, commit its current editing
+	for(int i = 0; i < current_controls.size(); i++) {
+	    MyArgumentControl control = (MyArgumentControl) current_controls.get(i);
+	    JComponent value_control = control.getValueControl();
+	    if(value_control instanceof JSpinner) {
+		try {
+		    ((JSpinner)value_control).commitEdit();
+		}
+		catch(Exception exception) {
+		    DebugStream.println("Exception in OptionsPane.mouseExited() - unexpected");
+		    DebugStream.printStackTrace(exception);
+		}
+	    }
+	    value_control = null;
+	    control = null;
+	}
+    }
+
+    /** Implementation side-effect
+     * @param e a MouseEvent
+     */
+    public void mousePressed(MouseEvent e) {}
+
+    /** Implementation side-effect
+     * @param e a MouseEvent
+     */
+    public void mouseReleased(MouseEvent e) {}
+
+    private class MyArgumentControl
+	extends ArgumentControl {
+	private int type;
+
+	public MyArgumentControl(int type, Argument argument, boolean enable, String value) {
+	    super(argument, enable, value);
+	    this.type = type;
+	}
+
+	/** Update the values stored in the collection so as to remember the current state of this argument. */
+	public void update() {
+	    String name = getArgumentName();
+	    boolean enable = isEnabled();
+	    String value = getValue();
+	    // If this argument was a flag, but is now disabled, remove from the build options altogether
+	    if(!enable && value == null) {
+		if(type == BUILD) {
+		    build_options.removeValue(name);
+		}
+		else if(type == SCHEDULE) {
+		    schedule_options.removeValue(name); 
+		}
+		else {
+		    import_options.removeValue(name);
+		}
+	    }
+	    // Otherwise update the argument value
+	    else {
+		if(type == BUILD) {
+		    build_options.setValue(name, enable, value);
+		}
+		else if(type == SCHEDULE) {
+		    schedule_options.setValue(name, enable, value); 
+		}
+		else {
+		    import_options.setValue(name, enable, value);
+		}
+	    }
+	}
+    }
+
+    /** Holds a File which has a particular naming convention build_log.date.txt also keeps a Date corresponding to the date in its name*/
+    private class FileEntry {
+
+       private AppendLineOnlyFileDocument current_document;
+       private Date date;
+       private long last_modified;
+       private String display;
+       private String filename;
+       private String filepath;
+       
+       public FileEntry(String filename, String filepath) {
+	  this.date = null;
+	  this.display = null;
+	  this.filename = filename;
+	  this.filepath = filepath;
+	  this.last_modified = 0L;
+       }
+
+	/** returns 0 if the dates are the same, -ve number if the current FileEntry is earlier than the fe FileEntry ...*/
+	public int compareTo(FileEntry file_entry) {
+	    Date our_date = getDate();
+	    Date other_date = file_entry.getDate();
+	    return our_date.compareTo(other_date);
+	}
+
+	public Date getDate() {
+	    if(date == null) {
+		// Need to exclude first '.'
+		int first_index = filename.indexOf(".") + 1;
+		// Need to exclude the last '.'
+		int last_index = filename.lastIndexOf(".");
+		if(first_index > 0 && last_index > 0 && first_index < last_index) {
+		    String date_string = filename.substring(first_index, last_index);
+		    date = new Date(Long.parseLong(date_string));
+		}
+		else {
+		    date = new Date(); // Current date
+		}
+	    }
+	    return date;
+	}
+
+       public AppendLineOnlyFileDocument getDocument() {
+	  if(current_document == null) {
+	     current_document = new AppendLineOnlyFileDocument(filepath);
+	  }
+	  return current_document;
+	}
+
+	public void reset() {
+	    display = null;
+	}
+
+	/** we only want the date out of the file name, not the whole path */
+	public String toString() {
+	    File file = new File(filename);
+	    if(display == null) {
+		last_modified = file.lastModified();
+		StringBuffer d = new StringBuffer();
+		Date date = getDate();
+		d.append(date.toString());
+		char success = UNKNOWN;
+		File the_file = new File(filepath);
+		if(the_file.exists()) {
+		   try {
+		      FileInputStream in = new FileInputStream(the_file);
+		      success = (char) in.read();
+		      in.close();
+		      in = null;
+		   }
+		   catch(Exception error) {
+		      ///ystem.err.println("Log '" + filepath + "' not found!");
+		      ///atherer.printStackTrace(error);
+		   }
+		}
+		the_file = null;
+		switch (success) {
+		case SUCCESSFUL:
+		    d.append(Dictionary.get("OptionsPane.Successful"));
+		    break;
+		case UNSUCCESSFUL:
+		    d.append(Dictionary.get("OptionsPane.Unsuccessful"));
+		    break;
+		case CANCELLED:
+		    d.append(Dictionary.get("OptionsPane.Cancelled"));
+		    break;
+		case SCHEDULED:
+		    d.append(Dictionary.get("OptionsPane.Scheduled"));
+		    break;
+		default:
+		    d.append(Dictionary.get("OptionsPane.Unknown"));
+		}
+		display = d.toString();
+	    }
+	    return display;
+	}
+    }
+
+    /** a ListSelectionListener that triggers the load of a newly selected log */
+    private class LogListListener implements ListSelectionListener {
+
+	public void valueChanged(ListSelectionEvent e) {
+	    if (!e.getValueIsAdjusting()) { // we get two events for one change in list selection - use the false one ( the second one)
+		///ystem.err.println("Log change detected.");
+		JList source  = (JList)e.getSource();
+		file_entry = (FileEntry) source.getSelectedValue();
+		// First we determine if the old log has been completely written to file
+		Document document = log_textarea.getDocument();
+		///ystem.err.println(" * current document: " + document);
+		///ystem.err.println(" * new document:     " + file_entry.getDocument());
+		// If we are dealing with the same document don't do anything.
+		if(document != file_entry.getDocument()) {
+		    if(document instanceof AppendLineOnlyFileDocument) {
+			AppendLineOnlyFileDocument append_line_only_file_document = (AppendLineOnlyFileDocument) document;
+			if(append_line_only_file_document.isStillWriting()) {
+			    ///ystem.err.println("Current log is still active... finishing.");
+			    writing_documents.add(append_line_only_file_document); // We have to maintain a reference until they are all done.
+			    append_line_only_file_document.setOwner(OptionsPane.this);
+			    append_line_only_file_document.setExit();
+			}
+			else {
+			    ///ystem.err.println("Current log is complete. Nothing to do.");
+			}
+		    }
+		    // Load the new log
+		    log_textarea.setDocument(file_entry.getDocument());
+		}
+	    }
+	}
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/Preferences.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/Preferences.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/Preferences.java	(revision 31635)
@@ -0,0 +1,1152 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.plaf.*;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.GAuthenticator;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.ClassifierManager;
+import org.greenstone.gatherer.cdm.LanguageManager;
+import org.greenstone.gatherer.cdm.PluginManager;
+import org.greenstone.gatherer.collection.Collection;
+import org.greenstone.gatherer.gui.tree.DragTree;
+import org.greenstone.gatherer.util.ArrayTools; // just for debug
+import org.greenstone.gatherer.util.CheckList;
+import org.greenstone.gatherer.util.CheckListEntry;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+public class Preferences
+    extends ModalDialog
+{
+    static final public String CONNECTION_PREFS = "connection";
+    static final public String GENERAL_PREFS = "general";
+
+    static final private Dimension LABEL_SIZE = new Dimension(280, 25);
+    static final private Dimension ROW_SIZE = new Dimension(640, 25);
+    static final private Dimension SIZE = new Dimension(640, 345);
+    static final private String TRUE = "true";
+
+    private CheckList warning_preferences_check_list;
+    private EmailField email_field;
+    private JButton apply_button;
+    private JButton cancel_button;
+    private JButton chdir_button;
+    private JButton ok_button;
+    private JCheckBox show_file_size_checkbox;
+    private JCheckBox use_proxy_checkbox;
+    private JCheckBox view_extracted_metadata_checkbox;
+    private JCheckBox workflow_download;
+    private JCheckBox workflow_gather;
+    private JCheckBox workflow_enrich;
+    private JCheckBox workflow_design;
+    private JCheckBox workflow_create;
+    private JCheckBox workflow_format;
+    private JComboBox language_combobox;
+    private JComboBox servlet_combobox; // GS3
+    private JComboBox site_combobox; // GS3
+    private JRadioButton assistant_mode_radio_button;
+    private JRadioButton expert_mode_radio_button;
+    private JRadioButton librarian_mode_radio_button;
+    private JSpinner proxy_port_field;
+    private JTabbedPane tab_pane;
+    private JTextArea mode_description_textarea;
+    private JTextField font_field;
+    private JTextField gliserver_url_field;
+    private JTextField library_path_field;
+    private JTextField program_field;
+    private JTextField proxy_host_field;
+    private JTextField collect_dir_field;
+
+    private Preferences self;
+
+    private String current_site_selection;
+
+    public Preferences() {
+	this(GENERAL_PREFS);
+    }
+
+    public Preferences(String initial_view) {
+	// Initialize
+	super(Gatherer.g_man, true);
+	this.self = this;
+	setSize(SIZE);
+	setTitle(Dictionary.get("Preferences"));
+	setJMenuBar(new SimpleMenuBar("preferences"));
+
+	// Creation
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	tab_pane = new JTabbedPane();
+        tab_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel general_preferences = createGeneralPreferences();
+        general_preferences.setComponentOrientation(Dictionary.getOrientation());
+	tab_pane.addTab(Dictionary.get("Preferences.General"), null, general_preferences, Dictionary.get("Preferences.General_Tooltip")); 
+	tab_pane.addTab(Dictionary.get("Preferences.Mode"), null, createModePreferences(), Dictionary.get("Preferences.Mode_Tooltip"));
+	//	tab_pane.addTab(Dictionary.get("Preferences.Workflow"), null, createWorkflowPreferences(), Dictionary.get("Preferences.Workflow_Tooltip")); 
+	JPanel connection_preferences = createConnectionPreferences();
+        connection_preferences.setComponentOrientation(Dictionary.getOrientation());
+	tab_pane.addTab(Dictionary.get("Preferences.Connection"), null, connection_preferences, Dictionary.get("Preferences.Connection_Tooltip")); 
+	tab_pane.addTab(Dictionary.get("Preferences.Warnings"), null, createWarningPreferences(), Dictionary.get("Preferences.Warnings_Tooltip")); 
+	
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+	
+	apply_button = new GLIButton(Dictionary.get("General.Apply"), Dictionary.get("General.Apply_Tooltip"));
+	
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+	
+	// Connection
+	ok_button.addActionListener(new OKButtonListener(true));
+	apply_button.addActionListener(new OKButtonListener(false));
+	cancel_button.addActionListener(new CancelButtonListener());
+
+	// Layout
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,2,2,2));
+	button_pane.setLayout(new GridLayout(1,3,0,5));
+	button_pane.add(ok_button);
+	button_pane.add(apply_button);
+	button_pane.add(cancel_button);
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(tab_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	Dimension frame_size = Gatherer.g_man.getSize();
+	Point frame_location = Gatherer.g_man.getLocation();
+	setLocation(((frame_size.width - SIZE.width) / 2), ((frame_size.height - SIZE.height)));
+
+	// Bring the desired pane to the fore
+	if (initial_view.equals(CONNECTION_PREFS)) {
+	    tab_pane.setSelectedComponent(connection_preferences);
+	}
+	else {
+	    tab_pane.setSelectedComponent(general_preferences);
+	}
+	
+	// Clean up
+	general_preferences = null;
+	connection_preferences = null;
+	frame_location = null;
+	frame_size = null;
+	cancel_button = null;
+	ok_button = null;
+	button_pane = null;
+	tab_pane = null;
+	content_pane = null;
+
+	setVisible(true);
+    }
+
+    private JPanel createConnectionPreferences() {
+	JPanel program_pane = new JPanel();
+        program_pane.setComponentOrientation(Dictionary.getOrientation());
+	program_pane.setPreferredSize(ROW_SIZE);
+	JLabel program_label = new JLabel(Dictionary.get("Preferences.Connection.ProgramCommand"));
+	program_label.setPreferredSize(LABEL_SIZE);
+	program_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	program_field = new JTextField(Configuration.getPreviewCommand());
+	program_field.setCaretPosition(0);
+	program_field.setToolTipText(Dictionary.get("Preferences.Connection.ProgramCommand_Tooltip"));
+	program_field.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel library_path_pane = new JPanel();
+	library_path_pane.setPreferredSize(ROW_SIZE);
+        library_path_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel library_path_label = new JLabel();
+	library_path_label.setPreferredSize(LABEL_SIZE);
+        library_path_label.setComponentOrientation(Dictionary.getOrientation());
+	String library_url_string = "";
+	if (Configuration.library_url != null) {
+	    library_url_string = Configuration.library_url.toString();
+	}
+	library_path_field = new JTextField(library_url_string);
+	library_path_field.setCaretPosition(0);
+        library_path_field.setComponentOrientation(Dictionary.getOrientation());
+	if (Gatherer.GS3) {
+	    library_path_label.setText(Dictionary.get("Preferences.Connection.Library_Path_GS3"));
+	    library_path_field.setToolTipText(Dictionary.get("Preferences.Connection.Library_Path_Tooltip_GS3"));
+	} else {
+	    library_path_label.setText(Dictionary.get("Preferences.Connection.Library_Path"));
+	    library_path_field.setToolTipText(Dictionary.get("Preferences.Connection.Library_Path_Tooltip"));
+	}
+	    
+	// Disable this field when using the applet, as it is automatically determined
+	library_path_label.setEnabled(!Gatherer.isApplet);
+	library_path_field.setEnabled(!Gatherer.isApplet);
+
+	JPanel gliserver_url_pane = null;
+	JLabel gliserver_url_label = null;
+	if (Gatherer.isGsdlRemote && !Gatherer.GS3) {
+	    gliserver_url_pane = new JPanel();
+	    gliserver_url_pane.setPreferredSize(ROW_SIZE);
+	    gliserver_url_label = new JLabel(Dictionary.get("Preferences.Connection.GLIServer_URL"));
+	    gliserver_url_label.setPreferredSize(LABEL_SIZE);
+            gliserver_url_label.setComponentOrientation(Dictionary.getOrientation());
+	    String gliserver_url_string = "";
+	    if (Configuration.gliserver_url != null) {
+		gliserver_url_string = Configuration.gliserver_url.toString();
+	    }
+	    gliserver_url_field = new JTextField(gliserver_url_string);
+            gliserver_url_field.setComponentOrientation(Dictionary.getOrientation());
+	    gliserver_url_field.setCaretPosition(0);
+	    gliserver_url_field.setToolTipText(Dictionary.get("Preferences.Connection.GLIServer_URL_Tooltip"));
+	   
+	    // Disable this field when using the applet, as it is automatically determined
+	    gliserver_url_label.setEnabled(!Gatherer.isApplet);
+	    gliserver_url_field.setEnabled(!Gatherer.isApplet);
+	}
+
+	JPanel site_pane = null;
+	JLabel site_label = null;
+	JPanel servlet_pane = null;
+	JLabel servlet_label = null;
+	if (Gatherer.GS3) {
+	    site_pane = new JPanel();
+	    site_pane.setPreferredSize(ROW_SIZE);
+            site_pane.setComponentOrientation(Dictionary.getOrientation());
+	    site_label = new JLabel(Dictionary.get("Preferences.Connection.Site"));
+	    site_label.setPreferredSize(LABEL_SIZE);
+            site_label.setComponentOrientation(Dictionary.getOrientation());
+	    // what should we do if Gatherer.servlet_config.getSites() is null?
+	    site_combobox = new JComboBox(Gatherer.servlet_config.getSites().toArray());
+	    site_combobox.setOpaque(false);
+	    site_combobox.setToolTipText(Dictionary.get("Preferences.Connection.Site_Tooltip"));
+	    site_combobox.setComponentOrientation(Dictionary.getOrientation());
+            
+	    servlet_pane = new JPanel();
+	    servlet_pane.setPreferredSize(ROW_SIZE);
+            servlet_pane.setComponentOrientation(Dictionary.getOrientation());
+            
+	    servlet_label = new JLabel(Dictionary.get("Preferences.Connection.Servlet"));
+	    servlet_label.setPreferredSize(LABEL_SIZE);
+            servlet_label.setComponentOrientation(Dictionary.getOrientation());
+	    servlet_combobox = new JComboBox();
+	    servlet_combobox.setOpaque(false);
+            servlet_combobox.setComponentOrientation(Dictionary.getOrientation());
+	    // try to locate and select the current site
+	    String this_site = Configuration.site_name;
+	    for(int b = 0; b < site_combobox.getItemCount(); b++) {
+		String entry = (String) site_combobox.getItemAt(b);
+		if(this_site.equals(entry)) {
+		    site_combobox.setSelectedIndex(b);
+		    break;
+		}
+	    }
+
+	    // just in case its not the current one?
+	    current_site_selection = (String)site_combobox.getSelectedItem();
+
+	    ArrayList servlet_options = Gatherer.servlet_config.getServletsForSite(current_site_selection);
+	    if (servlet_options == null) {
+		servlet_combobox.setToolTipText(Dictionary.get("Preferences.Connection.Servlet_Tooltip2"));
+		//servlet_combobox.setModel(new DefaultComboBoxModel());
+		servlet_combobox.setEnabled(false);
+	    } else {
+		///ystem.err.println(ArrayTools.objectArrayToString(servlet_options.toArray()));
+
+		servlet_combobox.setModel(new DefaultComboBoxModel(servlet_options.toArray()));
+		servlet_combobox.setToolTipText(Dictionary.get("Preferences.Connection.Servlet_Tooltip"));
+		servlet_combobox.setEnabled(true);
+		// try to locate and select the current servlet
+		String this_servlet = Configuration.getServletPath();
+		for(int b = 0; b < servlet_combobox.getItemCount(); b++) {
+		    String entry = (String) servlet_combobox.getItemAt(b);
+		    if(this_servlet.equals(entry)) {
+			servlet_combobox.setSelectedIndex(b);
+			break;
+		    }
+		}
+		
+	    }
+	}
+
+	JPanel collect_dir_pane = new JPanel();
+        collect_dir_pane.setComponentOrientation(Dictionary.getOrientation());
+	collect_dir_pane.setPreferredSize(ROW_SIZE);
+	JLabel collect_dir_label = new JLabel(Dictionary.get("Preferences.Connection.CollectDirectory"));
+	collect_dir_label.setPreferredSize(LABEL_SIZE);
+	collect_dir_label.setComponentOrientation(Dictionary.getOrientation());
+	collect_dir_field = new JTextField(Gatherer.getCollectDirectoryPath());
+	//collect_dir_field = new JTextField(Configuration.getString("general.open_collection"+Configuration.gliPropertyNameSuffix(), true));
+	collect_dir_field.setCaretPosition(0);
+	collect_dir_field.setToolTipText(Dictionary.get("Preferences.Connection.CollectDirectory_Tooltip"));
+	collect_dir_field.setEditable(false);
+	JButton chdir_button = new GLIButton(Dictionary.get("General.CD"), Dictionary.get("General.CD_Tooltip"));
+	chdir_button.addActionListener(new ChangeDirListener());
+	if(Gatherer.isGsdlRemote) { // disable changing directories for client GLI
+	    chdir_button.setEnabled(false);
+	}
+	
+	boolean currently_enabled = Configuration.get("general.use_proxy", true);
+	// Creation
+	JPanel connection_pane = new JPanel();
+        connection_pane.setComponentOrientation(Dictionary.getOrientation());
+	use_proxy_checkbox = new JCheckBox(Dictionary.get("Preferences.Connection.Use_Proxy"));
+	use_proxy_checkbox.setSelected(currently_enabled);
+	use_proxy_checkbox.setComponentOrientation(Dictionary.getOrientation());        
+	use_proxy_checkbox.setPreferredSize(ROW_SIZE);
+	
+        JPanel proxy_host_pane = new JPanel();
+        proxy_host_pane.setComponentOrientation(Dictionary.getOrientation());
+	proxy_host_pane.setPreferredSize(ROW_SIZE);
+        
+	JLabel proxy_host_label = new JLabel(Dictionary.get("Preferences.Connection.Proxy_Host"));
+        proxy_host_label.setComponentOrientation(Dictionary.getOrientation());
+	proxy_host_label.setPreferredSize(LABEL_SIZE);
+	
+	proxy_host_field = new JTextField(Configuration.getString("general.proxy_host", true));
+	proxy_host_field.setEnabled(currently_enabled);
+	proxy_host_field.setToolTipText(Dictionary.get("Preferences.Connection.Proxy_Host_Tooltip"));
+	proxy_host_field.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel proxy_port_pane = new JPanel();
+        proxy_port_pane.setComponentOrientation(Dictionary.getOrientation());
+	proxy_port_pane.setPreferredSize(ROW_SIZE);
+        
+	JLabel proxy_port_label = new JLabel(Dictionary.get("Preferences.Connection.Proxy_Port"));
+	proxy_port_label.setPreferredSize(LABEL_SIZE);
+        proxy_port_label.setComponentOrientation(Dictionary.getOrientation());
+	
+	String port_value = Configuration.getString("general.proxy_port", true);
+	if(port_value.length() > 0) {
+	   proxy_port_field = new JSpinner(new SpinnerNumberModel((new Integer(port_value)).intValue(), 0, 65535, 1));
+	}
+	else {
+	   proxy_port_field = new JSpinner(new SpinnerNumberModel(0, 0, 65535, 1));
+	}
+	proxy_port_field.setEnabled(currently_enabled);
+	proxy_port_field.setToolTipText(Dictionary.get("Preferences.Connection.Proxy_Port_Tooltip"));
+	
+	// Connection
+	use_proxy_checkbox.addActionListener(new UseProxyListener());
+	if (Gatherer.GS3) {
+	    site_combobox.addActionListener(new SiteComboboxListener());
+	}
+
+	// Layout
+	program_pane.setLayout(new BorderLayout());
+	program_pane.add(program_label, BorderLayout.LINE_START);
+	program_pane.add(program_field, BorderLayout.CENTER);
+
+	library_path_pane.setLayout(new BorderLayout());
+	library_path_pane.add(library_path_label, BorderLayout.LINE_START);
+	library_path_pane.add(library_path_field, BorderLayout.CENTER);
+
+	if (Gatherer.isGsdlRemote && !Gatherer.GS3) {
+	    gliserver_url_pane.setLayout(new BorderLayout());
+	    gliserver_url_pane.add(gliserver_url_label, BorderLayout.LINE_START);
+	    gliserver_url_pane.add(gliserver_url_field, BorderLayout.CENTER);
+	}
+
+	if (Gatherer.GS3) {
+	    site_pane.setLayout(new BorderLayout());
+	    site_pane.add(site_label, BorderLayout.LINE_START);
+	    site_pane.add(site_combobox, BorderLayout.CENTER);
+
+	    servlet_pane.setLayout(new BorderLayout());
+	    servlet_pane.add(servlet_label, BorderLayout.LINE_START);
+	    servlet_pane.add(servlet_combobox, BorderLayout.CENTER);
+	}
+
+	collect_dir_pane.setLayout(new BorderLayout());
+	collect_dir_pane.add(collect_dir_label, BorderLayout.LINE_START);
+	collect_dir_pane.add(collect_dir_field, BorderLayout.CENTER);
+	collect_dir_pane.add(chdir_button, BorderLayout.LINE_END);
+
+	proxy_host_pane.setLayout(new BorderLayout());
+	proxy_host_pane.add(proxy_host_label, BorderLayout.LINE_START);
+	proxy_host_pane.add(proxy_host_field, BorderLayout.CENTER);
+
+	proxy_port_pane.setLayout(new BorderLayout());
+	proxy_port_pane.add(proxy_port_label, BorderLayout.LINE_START);
+	proxy_port_pane.add(proxy_port_field, BorderLayout.CENTER);
+
+	connection_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	connection_pane.setLayout(new GridLayout(8,1,0,2));
+	connection_pane.add(program_pane);
+	connection_pane.add(library_path_pane);
+	if (Gatherer.isGsdlRemote && !Gatherer.GS3) {
+	    connection_pane.add(gliserver_url_pane);
+	}
+	if (Gatherer.GS3) {
+	    connection_pane.add(site_pane);
+	    connection_pane.add(servlet_pane);
+	}
+	connection_pane.add(collect_dir_pane);
+
+	connection_pane.add(use_proxy_checkbox);
+	connection_pane.add(proxy_host_pane);
+	connection_pane.add(proxy_port_pane);
+
+	return connection_pane;
+    }
+
+    private JPanel createGeneralPreferences() {
+	JPanel general_pane = new JPanel();
+        general_pane.setComponentOrientation(Dictionary.getOrientation());
+	// Build the model of available languages
+	ArrayList dictionary_model = new ArrayList();
+
+	// The new method makes use of the successor to the languages.dat file, classes/xml/languages.xml
+	NodeList language_elements = LanguageManager.LANGUAGES_DOCUMENT.getDocumentElement().getElementsByTagName(StaticStrings.LANGUAGE_ELEMENT);
+	for(int i = 0; i < language_elements.getLength(); i++) {
+	    Element language_element = (Element) language_elements.item(i);
+	    if((language_element.hasAttribute(StaticStrings.GLI_ATTRIBUTE) 
+		&& (language_element.getAttribute(StaticStrings.GLI_ATTRIBUTE)).equalsIgnoreCase(StaticStrings.TRUE_STR)) 
+	       || (language_element.hasAttribute(StaticStrings.MDS_ATTRIBUTE) 
+		   && (language_element.getAttribute(StaticStrings.MDS_ATTRIBUTE)).equalsIgnoreCase(StaticStrings.TRUE_STR)))
+	    {
+		Locale locale = new Locale(language_element.getAttribute(StaticStrings.CODE_ATTRIBUTE));
+		String description = language_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+		DictionaryEntry entry = new DictionaryEntry(description, locale);
+		if(!dictionary_model.contains(entry)) {
+		    dictionary_model.add(entry);
+		}
+		entry = null;
+		description = null;
+		locale = null;
+	    }
+	    language_element = null;
+	}
+	language_elements = null;
+
+	// Users email
+	JPanel email_pane = new JPanel();
+        email_pane.setComponentOrientation(Dictionary.getOrientation());
+
+        JLabel email_label = new JLabel(Dictionary.get("Preferences.General.Email"));
+        email_label.setComponentOrientation(Dictionary.getOrientation());
+	email_label.setPreferredSize(LABEL_SIZE);
+	
+        email_field = new EmailField(Configuration.getColor("coloring.error_background", false));
+	email_field.setText(Configuration.getEmail());
+	email_field.setToolTipText(Dictionary.get("Preferences.General.Email_Tooltip"));
+	email_field.setComponentOrientation(Dictionary.getOrientation());
+	// Font selection
+	JPanel font_pane = new JPanel();
+        font_pane.setComponentOrientation(Dictionary.getOrientation());
+	
+        JLabel font_label = new JLabel(Dictionary.get("Preferences.General.Font"));
+        font_label.setComponentOrientation(Dictionary.getOrientation());
+	font_label.setPreferredSize(LABEL_SIZE);
+	
+        font_field = new JTextField(Configuration.getString("general.font", true));
+	font_field.setToolTipText(Dictionary.get("Preferences.General.Font_Tooltip"));
+	font_field.setComponentOrientation(Dictionary.getOrientation());
+        
+	// Extracted metadata
+	view_extracted_metadata_checkbox = new JCheckBox(Dictionary.get("Preferences.General.View_Extracted_Metadata"));
+	view_extracted_metadata_checkbox.setSelected(false);
+        view_extracted_metadata_checkbox.setComponentOrientation(Dictionary.getOrientation());
+	
+        if (Configuration.get("general.view_extracted_metadata", Configuration.COLLECTION_SPECIFIC)) {
+	    view_extracted_metadata_checkbox.setSelected(true);
+	}
+	view_extracted_metadata_checkbox.setToolTipText(Dictionary.get("Preferences.General.View_Extracted_Metadata_Tooltip"));
+        view_extracted_metadata_checkbox.setComponentOrientation(Dictionary.getOrientation());
+        
+	// Show file sizes
+	show_file_size_checkbox = new JCheckBox(Dictionary.get("Preferences.General.Show_File_Size"));
+	show_file_size_checkbox.setSelected(false);
+        show_file_size_checkbox.setComponentOrientation(Dictionary.getOrientation());
+        
+	if (Configuration.get("general.show_file_size", Configuration.COLLECTION_SPECIFIC)) {
+	    show_file_size_checkbox.setSelected(true);
+	}
+	show_file_size_checkbox.setToolTipText(Dictionary.get("Preferences.General.Show_File_Size_Tooltip"));
+	
+	// Language
+	JPanel language_pane = new JPanel();
+        language_pane.setComponentOrientation(Dictionary.getOrientation());
+
+        JLabel language_label = new JLabel(Dictionary.get("Preferences.General.Interface_Language"));
+        language_label.setComponentOrientation(Dictionary.getOrientation());
+	language_label.setPreferredSize(LABEL_SIZE);
+	
+	language_combobox = new JComboBox(dictionary_model.toArray());
+	language_combobox.setOpaque(false);
+	language_combobox.setToolTipText(Dictionary.get("Preferences.General.Interface_Language_Tooltip"));
+	language_combobox.setComponentOrientation(Dictionary.getOrientation());
+        
+	// Try to locate and select the current language
+	// Else default to English
+	String language_code = Configuration.getLanguage();
+	int defaultIndex = 0;
+	DictionaryEntry defaultEntry = null;
+
+	int b = 0;
+	for (; b < language_combobox.getItemCount(); b++) {
+	    DictionaryEntry entry = (DictionaryEntry) language_combobox.getItemAt(b);
+	    if (language_code.equalsIgnoreCase(entry.getLocale().getLanguage())) {
+		language_combobox.setSelectedIndex(b);
+		break;
+	    } else if (entry.getLocale().getLanguage().equalsIgnoreCase("en")) { // store English as fallback
+		defaultIndex = b;
+		defaultEntry = entry;
+	    }
+	}
+	// if we cycled through and couldn't find the chosen language to load, 
+	// then we set the Preferences' and Configuration's language to English
+	if (b == language_combobox.getItemCount()) {
+	    language_combobox.setSelectedIndex(defaultIndex);
+	    Configuration.setLocale("general.locale", true, defaultEntry.getLocale());
+	    System.err.println("*** GLI doesn't yet support the language: " + language_code 
+			       + " and has therefore defaulted to: " + Configuration.getLanguage());
+	}
+
+	// Layout
+	email_pane.setLayout(new BorderLayout());
+	email_pane.add(email_label, BorderLayout.LINE_START);
+	email_pane.add(email_field, BorderLayout.CENTER);
+
+	language_pane.setLayout(new BorderLayout());
+	language_pane.add(language_label, BorderLayout.LINE_START);
+	language_pane.add(language_combobox, BorderLayout.CENTER);
+
+	font_pane.setLayout(new BorderLayout());
+	font_pane.add(font_label, BorderLayout.LINE_START);
+	font_pane.add(font_field, BorderLayout.CENTER);
+
+	general_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	general_pane.setLayout(new GridLayout(5,1,0,5));
+	general_pane.add(email_pane);
+	general_pane.add(language_pane);
+	general_pane.add(font_pane);
+	general_pane.add(view_extracted_metadata_checkbox);
+	general_pane.add(show_file_size_checkbox);
+
+	return general_pane;
+    }
+
+    /** Generate the controls for altering the detail mode.
+     * @return a JPanel of controls
+     */
+    private JPanel createModePreferences() {
+	// Create Controls
+	JPanel mode_panel = new JPanel();
+        mode_panel.setComponentOrientation(Dictionary.getOrientation());
+	
+        JPanel button_panel = new JPanel();
+        button_panel.setComponentOrientation(Dictionary.getOrientation());
+	
+        ButtonGroup mode_button_group = new ButtonGroup();
+	
+        assistant_mode_radio_button = new JRadioButton(Dictionary.get("Preferences.Mode.Assistant"));
+	assistant_mode_radio_button.setOpaque(false);
+        assistant_mode_radio_button.setComponentOrientation(Dictionary.getOrientation());
+	mode_button_group.add(assistant_mode_radio_button);
+	
+        expert_mode_radio_button = new JRadioButton(Dictionary.get("Preferences.Mode.Expert"));
+	expert_mode_radio_button.setOpaque(false);
+        expert_mode_radio_button.setComponentOrientation(Dictionary.getOrientation());
+	mode_button_group.add(expert_mode_radio_button);
+	
+        librarian_mode_radio_button = new JRadioButton(Dictionary.get("Preferences.Mode.Librarian"));
+	librarian_mode_radio_button.setOpaque(false);
+        librarian_mode_radio_button.setComponentOrientation(Dictionary.getOrientation());
+	mode_button_group.add(librarian_mode_radio_button);
+	
+	
+        mode_description_textarea = new JTextArea();
+        mode_description_textarea.setComponentOrientation(Dictionary.getOrientation());
+	mode_description_textarea.setEditable(false);
+	mode_description_textarea.setLineWrap(true);
+	mode_description_textarea.setWrapStyleWord(true);
+	// Determine which value is already selected
+	switch(Configuration.getMode()) {
+	case Configuration.ASSISTANT_MODE:
+	    assistant_mode_radio_button.setSelected(true);
+	    mode_description_textarea.setText(Dictionary.get("Preferences.Mode.Assistant_Description"));
+	    break;
+	case Configuration.EXPERT_MODE:
+	    expert_mode_radio_button.setSelected(true);
+	    mode_description_textarea.setText(Dictionary.get("Preferences.Mode.Expert_Description"));
+	    break;
+	default:
+	    librarian_mode_radio_button.setSelected(true);
+	    mode_description_textarea.setText(Dictionary.get("Preferences.Mode.Librarian_Description"));
+	}
+	// Connection - when a radio button is selected we have to change the description
+	ModeRadioButtonListener listener = new ModeRadioButtonListener();
+	assistant_mode_radio_button.addActionListener(listener);
+	expert_mode_radio_button.addActionListener(listener);
+	librarian_mode_radio_button.addActionListener(listener);
+	listener = null;
+	// Layout
+	button_panel.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+	button_panel.setLayout(new GridLayout(3,1,2,2));
+	button_panel.add(assistant_mode_radio_button);
+	button_panel.add(librarian_mode_radio_button);
+	button_panel.add(expert_mode_radio_button);
+
+	mode_panel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	mode_panel.setLayout(new BorderLayout());
+	mode_panel.add(button_panel, BorderLayout.NORTH);
+	mode_panel.add(new JScrollPane(mode_description_textarea), BorderLayout.CENTER);
+
+	return mode_panel;
+    }
+
+
+    /** The warning preferences are controlled through a checklist. */
+    private JPanel createWarningPreferences()
+    {
+	warning_preferences_check_list = new CheckList(false);
+        warning_preferences_check_list.setComponentOrientation(Dictionary.getOrientation());
+
+	// Read all the warnings from the general xml/config.xml file, and their values from the user config.xml file
+	Document general_config_xml_file_document = XMLTools.parseXMLFile("xml/config.xml", true);
+	NodeList argument_elements_nodelist = general_config_xml_file_document.getDocumentElement().getElementsByTagName("Argument");
+	for (int i = 0; i < argument_elements_nodelist.getLength(); i++) {
+	    Element argument_element = (Element) argument_elements_nodelist.item(i);
+	    String argument_element_name = argument_element.getAttribute("name");
+	    if (argument_element_name.startsWith("warning.")) {
+		String warning_title = Dictionary.get(argument_element_name.substring("warning.".length()) + ".Title");
+		boolean warning_enabled = Configuration.get(argument_element_name, true);
+		CheckListEntry warning_entry = new CheckListEntry(warning_title, warning_enabled);
+		warning_entry.setProperty(argument_element_name);
+		warning_preferences_check_list.addEntry(warning_entry);
+	    }
+	}
+
+	JPanel warning_preferences_pane = new JPanel();
+        warning_preferences_pane.setComponentOrientation(Dictionary.getOrientation());
+	warning_preferences_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	warning_preferences_pane.setLayout(new BorderLayout());
+        JScrollPane scrol_tmp = new JScrollPane(warning_preferences_check_list);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	warning_preferences_pane.add(scrol_tmp, BorderLayout.CENTER);
+	return warning_preferences_pane;
+    }
+
+
+    private JPanel createWorkflowPreferences() {
+	// Read in the predefined configurations file
+	Vector predefined = new Vector();
+	Document predefined_document = XMLTools.parseXMLFile("xml/workflows.xml", true);
+	Element predefined_element = predefined_document.getDocumentElement();
+	NodeList workflow_elements = predefined_element.getElementsByTagName("Workflow");
+	for(int i = 0; i < workflow_elements.getLength(); i++) {
+	    predefined.add(new WorkflowElementWrapper((Element)workflow_elements.item(i)));
+	}
+
+	// Creation
+	JPanel workflow_preferences_pane = new JPanel();
+        workflow_preferences_pane.setComponentOrientation(Dictionary.getOrientation());
+	
+        JPanel checklist_pane = new JPanel();
+        checklist_pane.setComponentOrientation(Dictionary.getOrientation());
+	
+        JLabel title_label = new JLabel(Dictionary.get("Preferences.Workflow.Title"));
+        title_label.setComponentOrientation(Dictionary.getOrientation());
+	title_label.setPreferredSize(ROW_SIZE);
+	
+	workflow_download = new JCheckBox(Dictionary.get("GUI.Download")+" - "+Dictionary.get("GUI.Download_Tooltip"));
+        workflow_download.setComponentOrientation(Dictionary.getOrientation());
+	workflow_download.setSelected(Configuration.get("workflow.download", false) && Gatherer.isDownloadEnabled);
+	workflow_download.setPreferredSize(ROW_SIZE);
+	
+	workflow_gather = new JCheckBox(Dictionary.get("GUI.Gather")+" - "+Dictionary.get("GUI.Gather_Tooltip"));
+	workflow_gather.setSelected(Configuration.get("workflow.gather", false));
+	workflow_gather.setPreferredSize(ROW_SIZE);
+	workflow_gather.setComponentOrientation(Dictionary.getOrientation());
+        
+	workflow_enrich = new JCheckBox(Dictionary.get("GUI.Enrich")+" - "+Dictionary.get("GUI.Enrich_Tooltip"));
+	workflow_enrich.setSelected(Configuration.get("workflow.enrich", false));
+	workflow_enrich.setPreferredSize(ROW_SIZE);
+	workflow_enrich.setComponentOrientation(Dictionary.getOrientation());
+        
+	workflow_design = new JCheckBox(Dictionary.get("GUI.Design")+" - "+Dictionary.get("GUI.Design_Tooltip"));
+	workflow_design.setSelected(Configuration.get("workflow.design", false));
+	workflow_design.setPreferredSize(ROW_SIZE);
+	workflow_design.setComponentOrientation(Dictionary.getOrientation());
+        
+	workflow_create = new JCheckBox(Dictionary.get("GUI.Create")+" - "+Dictionary.get("GUI.Create_Tooltip"));
+	workflow_create.setSelected(Configuration.get("workflow.create", false));
+	workflow_create.setPreferredSize(ROW_SIZE);
+	workflow_create.setComponentOrientation(Dictionary.getOrientation());
+        
+	workflow_format = new JCheckBox(Dictionary.get("GUI.Format")+" - "+Dictionary.get("GUI.Format_Tooltip"));
+	workflow_format.setSelected(Configuration.get("workflow.format", false));
+	workflow_format.setPreferredSize(ROW_SIZE);
+	workflow_format.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel predefined_pane = new JPanel();
+        predefined_pane.setComponentOrientation(Dictionary.getOrientation());
+	
+        JLabel predefined_label = new JLabel(Dictionary.get("Preferences.Workflow.Predefined.Label"));
+        predefined_label.setComponentOrientation(Dictionary.getOrientation());
+	
+        JComboBox predefined_combobox = new JComboBox(predefined); 
+	predefined_combobox.setOpaque(false);
+	// Connection
+	predefined_combobox.addActionListener(new PredefinedActionListener());
+
+	// Layout
+	checklist_pane.setLayout(new BoxLayout(checklist_pane, BoxLayout.Y_AXIS));
+	checklist_pane.add(title_label);
+	if (Configuration.get("workflow.download", true)) {
+	    checklist_pane.add(workflow_download);
+	}
+	if (Configuration.get("workflow.gather", true)) {
+	    checklist_pane.add(workflow_gather);
+	}
+	if (Configuration.get("workflow.enrich", true)) {
+	    checklist_pane.add(workflow_enrich);
+	}
+	if (Configuration.get("workflow.design", true)) {
+	    checklist_pane.add(workflow_design);
+	}
+	if (Configuration.get("workflow.create", true)) {
+	    checklist_pane.add(workflow_create);
+	}
+	if (Configuration.get("workflow.format", true)) {
+	    checklist_pane.add(workflow_format);
+	}
+	
+	predefined_pane.setLayout(new BorderLayout(5,0));
+	predefined_pane.add(predefined_label, BorderLayout.LINE_START);
+	predefined_pane.add(predefined_combobox, BorderLayout.CENTER);
+
+	workflow_preferences_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	workflow_preferences_pane.setLayout(new BorderLayout());
+	workflow_preferences_pane.add(checklist_pane, BorderLayout.CENTER);
+	workflow_preferences_pane.add(predefined_pane, BorderLayout.SOUTH);
+
+	return workflow_preferences_pane;
+    }
+
+    private class ChangeDirListener implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    JFileChooser chooser = new JFileChooser(collect_dir_field.getText());
+	    chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+	    int returnVal = chooser.showOpenDialog(Preferences.this);
+	    if(returnVal == JFileChooser.APPROVE_OPTION) {
+		collect_dir_field.setText(chooser.getSelectedFile().getAbsolutePath() + File.separator);
+	    } // else collect_dir_field text remains as it is
+	}
+    }
+
+    private class OKButtonListener
+	implements ActionListener {
+	private boolean close;
+	public OKButtonListener(boolean close) {
+	    this.close = close;
+	}
+	public void actionPerformed(ActionEvent event)
+	{
+	    // Several options requiring restarting the GLI to apply: 
+	    // interface font, interface language, changed gliserver url or library url
+	    boolean restart_required = false;
+	    boolean keep_collection_open = false;
+	    String restart_message = "";
+
+	    
+	    // Connection preferences
+	    String program_str = program_field.getText();
+	    if (program_str.length() > 0 && program_str.indexOf("%1") == -1) {
+		program_str = program_str + " %1";
+	    }
+	    Configuration.setPreviewCommand(program_str);
+
+	    String old_library_url = (Configuration.library_url == null) ? null : Configuration.library_url.toString();
+	    String library_url_string = library_path_field.getText();
+	    if(old_library_url != null && !old_library_url.equals(library_url_string)) {
+
+		// If the server is remote and the library path's been changed, then GLI client needs restart
+		// If the server is local, GLI is going to work with the collection of GSDLPath anyway, 
+		// even if the library path for previewing has changed
+		if (Gatherer.isGsdlRemote) {
+		    restart_required = true;
+		    restart_message = Dictionary.get("Preferences.General.Manual_Restart_Required");
+		    
+		}
+		if(Gatherer.c_man.getCollection() != null) {
+		    // if the gliserver url has changed while a collection is open, 
+		    // close the open collection of the old library URL
+		    Gatherer.g_man.saveThenCloseCurrentCollection();
+		    Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), true, "");
+		}
+	    }
+	    if (library_url_string.equals("")) {
+		Configuration.library_url = null;
+	    }
+	    else {
+		try {
+		    Configuration.library_url = new URL(library_url_string);
+		}
+		catch (MalformedURLException exception) {
+		    DebugStream.printStackTrace(exception);
+		}
+	    }
+
+	    Configuration.setString("general.library_url"+Configuration.gliPropertyNameSuffix(), 
+				    true, library_url_string);
+
+	    if (Gatherer.isGsdlRemote && !Gatherer.GS3) {
+		String old_gliserver_url = Configuration.gliserver_url.toString(); 
+		String gliserver_url_string = gliserver_url_field.getText();
+		if(!old_gliserver_url.equals(gliserver_url_string)) {
+		    restart_required = true;
+		    restart_message = Dictionary.get("Preferences.General.Manual_Restart_Required");
+
+		    if(Gatherer.c_man.getCollection() != null) {
+			// if the gliserver url has changed while a collection is open, 
+			// close the open collection of the old gliserver URL
+			Gatherer.g_man.saveThenCloseCurrentCollection();
+			Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), true, "");
+		    }
+		}
+		if (gliserver_url_string.equals("")) {
+		    Configuration.gliserver_url = null;
+		}
+		else {
+		    try {
+			Configuration.gliserver_url = new URL(gliserver_url_string);
+		    }
+		    catch (MalformedURLException exception) {
+			DebugStream.printStackTrace(exception);
+		    }
+		}
+		Configuration.setString("general.gliserver_url", true, gliserver_url_string);
+	    }
+	    
+
+	    boolean site_changed = false;
+	    if (Gatherer.GS3) {
+		String current_site = Configuration.site_name;
+		String new_site =(String)site_combobox.getSelectedItem() ;
+		if (!new_site.equals(current_site)) {
+		    site_changed = true;
+		}
+		Configuration.setSiteAndServlet(new_site, (String)servlet_combobox.getSelectedItem());
+	    }
+	    
+	    Configuration.set("general.use_proxy", true, use_proxy_checkbox.isSelected());
+	    Configuration.setString("general.proxy_host", true, proxy_host_field.getText());
+	    Configuration.setString("general.proxy_port", true, proxy_port_field.getValue() + "");
+	    Gatherer.setProxy();
+
+	    // General preferences
+	    Configuration.setEmail(email_field.getText());
+	    Configuration.set("general.show_file_size", Configuration.COLLECTION_SPECIFIC, show_file_size_checkbox.isSelected());
+	    Configuration.set("general.view_extracted_metadata", Configuration.COLLECTION_SPECIFIC, view_extracted_metadata_checkbox.isSelected());
+
+
+	    // GLI interface font
+	    String current_font = Configuration.getString("general.font", true);
+	    if (!current_font.equals(font_field.getText())) {
+		Configuration.setString("general.font", true, font_field.getText());
+		restart_required = true;
+		restart_message = Dictionary.get("Preferences.General.Restart_Required");
+
+	    }
+
+	    // GLI interface language
+	    String current_lang = Configuration.getLanguage();
+	    if (!current_lang.equals(((DictionaryEntry) language_combobox.getSelectedItem()).getLocale().getLanguage())) {
+	      // need to save the collection before changing the locale otherwise it stuffs the metadata names up
+	      if(Gatherer.c_man.getCollection() != null) {
+		  Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), true, Gatherer.c_man.getLoadedCollectionColFilePath());
+		  Gatherer.g_man.saveThenCloseCurrentCollection();
+		  keep_collection_open = true;
+		}
+
+		Configuration.setLocale("general.locale", Configuration.GENERAL_SETTING, ((DictionaryEntry) language_combobox.getSelectedItem()).getLocale());
+		restart_required = true;
+		restart_message = Dictionary.get("Preferences.General.Restart_Required");
+	    }
+
+	    // Inform the user that a restart is required, if necessary
+	    if (restart_required) {
+		JOptionPane.showMessageDialog(Gatherer.g_man, restart_message, Dictionary.get("General.Warning"), JOptionPane.WARNING_MESSAGE);
+	    }
+
+	    // Mode preferences
+	    int current_mode = Configuration.getMode();
+	    int new_mode;
+	    if (assistant_mode_radio_button.isSelected()) {
+		new_mode = Configuration.ASSISTANT_MODE;
+	    }
+	    else if (expert_mode_radio_button.isSelected()) {
+		new_mode = Configuration.EXPERT_MODE;
+	    }
+	    else {
+		new_mode = Configuration.LIBRARIAN_MODE;
+	    }
+
+	    // If there has been a change in modes, update the config, and also inform the persistant gui views that have a need to know
+	    if (new_mode != current_mode) {
+		Configuration.setMode(new_mode);
+		Collection collection = Gatherer.c_man.getCollection();
+		if (collection != null) {
+		    collection.cdm.modeChanged(new_mode);
+		}
+		Gatherer.g_man.modeChanged(new_mode);
+	    }
+
+	    // Warning preferences
+	    ListModel warning_preferences_check_list_model = warning_preferences_check_list.getModel();
+	    for (int i = 0; i < warning_preferences_check_list_model.getSize(); i++) {
+		CheckListEntry entry = (CheckListEntry) warning_preferences_check_list_model.getElementAt(i);
+		Configuration.set(entry.getProperty(), true, entry.isSelected());
+	    }
+
+	    if (Gatherer.GS3 && site_changed && Gatherer.c_man.getCollection() != null && !Gatherer.isGsdlRemote) {
+		// shut down the collection
+		System.err.println("shutting down the collection");
+		Gatherer.g_man.saveThenCloseCurrentCollection();
+	    }
+
+	    // Workflow preferences
+// 	    Configuration.set("workflow.download", false, workflow_download.isSelected());
+// 	    Configuration.set("workflow.gather", false, workflow_gather.isSelected());
+// 	    Configuration.set("workflow.enrich", false, workflow_enrich.isSelected());
+// 	    Configuration.set("workflow.design", false, workflow_design.isSelected());
+// 	    Configuration.set("workflow.create", false, workflow_create.isSelected());
+// 	    Configuration.set("workflow.format", false, workflow_format.isSelected());
+	    
+// 	    Gatherer.g_man.workflowUpdate("Download", workflow_download.isSelected());
+// 	    Gatherer.g_man.workflowUpdate("Gather", workflow_gather.isSelected());
+// 	    Gatherer.g_man.workflowUpdate("Enrich", workflow_enrich.isSelected());
+// 	    Gatherer.g_man.workflowUpdate("Design", (workflow_design.isSelected() && Configuration.getMode() > Configuration.ASSISTANT_MODE));
+// 	    Gatherer.g_man.workflowUpdate("Create", workflow_create.isSelected());
+// 	    Gatherer.g_man.workflowUpdate("Format", workflow_format.isSelected());
+
+	    // Always save configuration changes immediately (in case the GLI crashes)
+	    Configuration.save();
+	    if (Gatherer.isGsdlRemote && Gatherer.GS3 && site_changed ){
+		Gatherer.remoteGreenstoneServer.downloadCollectionConfigurations();
+	    }
+	    // Refresh the GLI to account for the configuration changes
+	    Gatherer.refresh(Gatherer.PREFERENCES_CHANGED);
+
+	    // If proxy is on but proxy details are incomplete, then can't continue 
+	    if (use_proxy_checkbox.isSelected() && proxy_host_field.getText().equals("")) {
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("Preferences.Connection.Proxy_Host_Missing"),
+					      Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	    } else {
+		// Hide dialog
+		if (close) {
+		    self.dispose();
+		}
+	    }
+
+		// collect directory change
+	    String newCollectPath = collect_dir_field.getText();
+	    if(!newCollectPath.endsWith(File.separator)) {
+		newCollectPath += File.separator;
+	    }
+		// wait cursor will display while changing the collect directory, need to work out what component to
+		// display it on: main GLI frame or Preferences window, depending on if OK or Apply was pressed
+		Container container = close ? Gatherer.g_man.getContentPane() : Preferences.this.getContentPane();
+	    Gatherer.collectDirectoryHasChanged(Gatherer.getCollectDirectoryPath(), 
+				newCollectPath, container);
+			// will tell the server that the collect directory has changed and that
+			// the workspace needs to be refreshed (Documents in Greenstone Collections)	   
+		
+	    if (restart_required) {
+	      if (keep_collection_open) {
+		Gatherer.g_man.exitNoCollectionSave(Gatherer.EXIT_THEN_RESTART);
+	      } else {
+		Gatherer.g_man.exit(Gatherer.EXIT_THEN_RESTART);
+	      }
+	    }
+	}
+    }
+
+    private class CancelButtonListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    self.dispose();
+	}
+    }
+
+    private class DictionaryEntry
+	implements Comparable {
+	private Locale locale;
+	private String description;
+	public DictionaryEntry(Locale locale) {
+	    this.description = null;
+	    this.locale = locale;
+	}
+	public DictionaryEntry(String description, Locale locale) {
+	    this.description = description;
+	    this.locale = locale;
+	}
+	public int compareTo(Object object) {
+	    return toString().compareTo(object.toString());
+	}
+	public boolean equals(Object object) {
+	    return toString().equals(object.toString());
+	}
+	public Locale getLocale() {
+	    return locale;
+	}
+	public String toString() {
+	    if(description != null) {
+		return description;
+	    }
+	    else {
+		return locale.getDisplayName();
+	    }
+	}
+    }
+
+    /** This listener updates the mode description depending on what mode is selected. */
+    private class ModeRadioButtonListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    Object source = event.getSource();
+	    if(source == assistant_mode_radio_button) {
+		mode_description_textarea.setText(Dictionary.get("Preferences.Mode.Assistant_Description"));
+	    }
+	    else if(source == expert_mode_radio_button) {
+		mode_description_textarea.setText(Dictionary.get("Preferences.Mode.Expert_Description"));
+	    }
+	    else {
+		mode_description_textarea.setText(Dictionary.get("Preferences.Mode.Librarian_Description"));
+	    }
+	    source = null;
+	}
+    }
+
+    private class PredefinedActionListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    JComboBox cb = (JComboBox) event.getSource();
+	    WorkflowElementWrapper element = (WorkflowElementWrapper) cb.getSelectedItem();
+	    CheckboxUpdater task = new CheckboxUpdater(element);
+	    SwingUtilities.invokeLater(task);
+	}
+
+	private class CheckboxUpdater
+	    implements Runnable {
+	    private WorkflowElementWrapper element;
+	    public CheckboxUpdater(WorkflowElementWrapper element) {
+		this.element = element;
+	    }
+	    public void run() {
+		workflow_download.setSelected(element.getEnabled("download"));
+		workflow_gather.setSelected(element.getEnabled("gather"));
+		workflow_enrich.setSelected(element.getEnabled("enrich"));
+		workflow_design.setSelected(element.getEnabled("design"));
+		workflow_create.setSelected(element.getEnabled("create"));
+		workflow_format.setSelected(element.getEnabled("format"));
+	    }
+	}
+    }
+
+
+    private class SiteComboboxListener
+	implements ActionListener {
+	private boolean ignore_event=false;
+	public void actionPerformed(ActionEvent event) {
+	    System.err.println("event occurred "+event.paramString());
+	    String site = (String) site_combobox.getSelectedItem();
+	    System.err.println("The site changed to = "+site);
+	    if (!site.equals(current_site_selection)) {
+		current_site_selection = site;
+		System.err.println("changed the current selection");
+		
+		ArrayList servlet_options = Gatherer.servlet_config.getServletsForSite(current_site_selection);
+		if (servlet_options == null) {
+		    ///ystem.err.println("no servlets for this site");
+		    servlet_combobox.setModel(new DefaultComboBoxModel());
+		    servlet_combobox.setToolTipText(Dictionary.get("Preferences.Connection.Servlet_Tooltip2"));
+		    servlet_combobox.setEnabled(false);
+
+		} else {
+		    ///ystem.err.println(ArrayTools.objectArrayToString(servlet_options.toArray()));
+		    servlet_combobox.setModel(new DefaultComboBoxModel(servlet_options.toArray()));
+		    servlet_combobox.setToolTipText(Dictionary.get("Preferences.Connection.Servlet_Tooltip"));
+		    servlet_combobox.setEnabled(true);
+		}
+		if (Gatherer.isGsdlRemote){
+		    // Close the current collection, remove the lock on this file, ask to login to the new site, then download collection configurations of the site.
+		    if (Gatherer.c_man.getCollection()!=null){
+			File lock_file = new File(Gatherer.c_man.getLoadedCollectionDirectoryPath() + "gli.lck");
+			Gatherer.remoteGreenstoneServer.deleteCollectionFile(Gatherer.c_man.getLoadedCollectionName(),lock_file);
+			Gatherer.g_man.closeCurrentCollection();
+		    }
+		    Configuration.site_name=site;
+		    Gatherer.remoteGreenstoneServer.set_remote_greenstone_server_authentication_to_null();
+		    Gatherer.remoteGreenstoneServer.downloadCollectionConfigurations();
+		}
+	    }
+	}    
+    }
+
+    private class UseProxyListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    boolean enabled = use_proxy_checkbox.isSelected();
+	    Configuration.set("general.use_proxy", true, enabled);
+	    // Fortunately this is already driven by the event thread.
+	    proxy_host_field.setEnabled(enabled);
+	    proxy_port_field.setEnabled(enabled);
+	}
+    }
+
+    private class WorkflowElementWrapper {
+	private Element element;
+	private String text;
+	public WorkflowElementWrapper(Element element) {
+	    this.element = element;
+	}
+	public boolean getEnabled(String name) {
+	    boolean result = true;
+	    if(element.getAttribute(name).equalsIgnoreCase("false")) {
+		result = false;
+	    }
+	    return result;
+	}
+	public String toString() {
+	    if (text == null) {
+		text = Dictionary.get(element.getFirstChild().getNodeValue());
+	    }
+	    return text;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/PreviewButton.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/PreviewButton.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/PreviewButton.java	(revision 31635)
@@ -0,0 +1,173 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.greenstone.LocalLibraryServer;
+import org.greenstone.gatherer.greenstone3.ServletConfiguration;
+import org.greenstone.gatherer.util.StaticStrings;
+
+
+public class PreviewButton 
+    extends GLIButton {
+
+
+    static final public int HOME_PAGE = 1;
+    static final public int SEARCH_PAGE = 2;
+    static final public int CLASSIFIER_PAGE = 3;
+    static final public int DOCUMENT_PAGE = 4;
+
+    private String preview_address;
+    private int page_type = HOME_PAGE;
+    private boolean variable_preview = false;
+    
+    private PreviewButtonOwner owner = null;
+    public PreviewButton() {
+	super();
+	PreviewButtonListener pbl = new PreviewButtonListener();
+	addActionListener(pbl);
+    }
+    public PreviewButton(String text, String tooltip) {
+	super(text, tooltip);
+	PreviewButtonListener pbl = new PreviewButtonListener();
+	addActionListener(pbl);
+    }
+
+    public void setOwner(PreviewButtonOwner o) {
+	owner = o;
+    }
+    public void setVariablePreview(boolean vp) {
+	variable_preview = vp;
+    }
+    protected void configureHomeURL() {
+	// we could be working with standalone collections or collection groups
+	// so getting a collection-name may have a collection group prefix
+	// This means CollectionManager.getLoadedCollectionName() needs to (and does) return (colgroup/)subcolname
+	String collGroupWithName = CollectionManager.getLoadedCollectionName(true);
+
+	// set up the home page for the current collection
+	if (Gatherer.GS3) { // GLI for GS3 case
+	    if(Configuration.library_url == null) {// && !Configuration.fedora_info.isActive()) {
+		Gatherer.missingEXEC();
+	    } 
+	    // if we now finally have a library URL, then continue with previewing
+	    if(Configuration.library_url != null) {
+		// don't do anything fancy here
+		preview_address = Configuration.library_url.toString() + Configuration.getServletPath() 
+		    + "?a=p&sa=about&c=" + collGroupWithName + "&l=" + Configuration.getLanguage();
+	    }
+	    return;
+	}
+
+	if(Gatherer.isLocalLibrary) {
+	    // check that the local library server is still running  - doesn't do anything if it's not supposed to be running
+	    // !! Don't like this here
+	    LocalLibraryServer.checkServerRunning();
+	    if(LocalLibraryServer.isURLPending()) {
+		// Remind the user to press Enter Library, and return from this method
+		JOptionPane.showMessageDialog(Gatherer.g_man,
+					      Dictionary.get("General.LLS_Not_Started"), 
+					      Dictionary.get("General.LLS_Not_Started_Title"),
+					      JOptionPane.INFORMATION_MESSAGE);
+		preview_address = null;
+		return;	  
+	    } 
+	}
+	// FLI or GLI for GS2 case (no library_url)
+	if(Configuration.fedora_info.isActive()) {
+	    preview_address = Configuration.library_url.toString() + "?pid=true&title=true&terms=&query=pid~greenstone:"+collGroupWithName+"*&maxResults=20";
+	    //+ "&l=" + Configuration.getLanguage();
+	} else {
+	    preview_address = Configuration.library_url.toString() + "?c=" + collGroupWithName + "&l=" + Configuration.getLanguage();
+	}
+
+	String main_args = "&a=p&p=about";
+	/* this code is for switching the preview page depending on what 
+	    is currently active. but it is not complete yet
+	String main_args = null;
+	String params = null;
+	if (!variable_preview || owner == null) {
+	    main_args = "&a=p&p=about";
+	} else {
+	    page_type = owner.getPageType();
+	    params = owner.getPageParams();
+	    
+	    switch (page_type) {
+	    case SEARCH_PAGE:
+		main_args = "&a=q&q="+params;
+		break;
+	    case CLASSIFIER_PAGE:
+		main_args = "&a=d&cl="+params;
+		break;
+	    case DOCUMENT_PAGE:
+		  main_args = "&a=d&d="+params;
+		break;
+	    case HOME_PAGE:
+	    default:
+		main_args = "&a=p&p=about";
+		
+		
+	    }
+	    
+
+	}
+	*/
+	preview_address += main_args+StaticStrings.TIMESTAMP_ARGUMENT + System.currentTimeMillis();
+	
+    }
+    private class PreviewButtonListener
+	implements ActionListener {
+
+	public void actionPerformed(ActionEvent event) {
+	    Gatherer.c_man.saveCollection();
+	    
+	    configureHomeURL();
+	    
+	    if (Gatherer.GS3) {
+		// we need to rerun the convert in case the format stuff has changed
+		//for gs3, we don't need this anymore: Gatherer.c_man.convertToGS3Collection();
+		Gatherer.configGS3Server(Configuration.site_name, ServletConfiguration.ADD_COMMAND + Gatherer.c_man.getLoadedCollectionName());
+	    }
+	    if(preview_address != null) {
+		Gatherer.spawnBrowser(preview_address);
+	    }
+
+	}
+
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/PreviewButtonOwner.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/PreviewButtonOwner.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/PreviewButtonOwner.java	(revision 31635)
@@ -0,0 +1,40 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+public interface PreviewButtonOwner {
+
+    public int getPageType();
+
+    public String getPageParams();
+
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/PreviewCommandDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/PreviewCommandDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/PreviewCommandDialog.java	(revision 31635)
@@ -0,0 +1,268 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.File;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.filechooser.*;
+import javax.swing.table.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.Utility;
+
+/** This dialog allows the user to choose which browser to use for the preview command 
+ * @author Katherine Don, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class PreviewCommandDialog
+    extends ModalDialog {
+
+    /** The default size for the dialog. */
+    static final private Dimension SIZE = new Dimension(500, 195);
+
+    private PreviewCommandDialog self;
+    private JButton browse_button;
+    private JButton cancel_button;
+    private JButton ok_button;
+    private JTextField command_field;
+    private String preview_command=null;
+    /** Create a new PreviewCommandDialog
+     */
+    public PreviewCommandDialog() {
+	super(Gatherer.g_man);
+	this.self = this;
+        JScrollPane scrol_tmp;
+        this.setComponentOrientation(Dictionary.getOrientation());
+	// Creation
+	setModal(true);
+	setSize(SIZE);
+	setJMenuBar(new SimpleMenuBar("thepreviewview"));
+	setTitle(Dictionary.get("PreviewCommandDialog.Title"));
+	
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+
+	JTextArea instructions_area = new JTextArea(Dictionary.get("PreviewCommandDialog.Instructions"));
+        instructions_area.setComponentOrientation(Dictionary.getOrientation());
+        instructions_area.setEditable(false);
+	instructions_area.setLineWrap(true);
+	instructions_area.setRows(5);
+	instructions_area.setWrapStyleWord(true);
+	
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel lower_pane = new JPanel();
+        lower_pane.setComponentOrientation(Dictionary.getOrientation());
+
+	JPanel command_pane = new JPanel();
+        command_pane.setComponentOrientation(Dictionary.getOrientation());
+	command_field = new JTextField();
+        command_field.setComponentOrientation(Dictionary.getOrientation());
+	browse_button = new GLIButton(Dictionary.get("FileAssociationDialog.Browse"));
+	browse_button.setEnabled(!Utility.isMac());
+	if (Utility.isMac()) {
+	    browse_button.setToolTipText(Dictionary.get("FileAssociationDialog.Browse_Tooltip_Mac"));
+	} else {
+	    browse_button.setToolTipText(Dictionary.get("FileAssociationDialog.Browse", "FileAssociationDialog.Browse_Tooltip"));
+	}
+	ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("FileAssociationDialog.Close_Tooltip"));
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Pure_Cancel_Tooltip"));
+	
+	// Connection
+	browse_button.addActionListener(new BrowseButtonListener());
+	ok_button.addActionListener(new OkButtonListener());
+	cancel_button.addActionListener(new CancelButtonListener());
+	
+	// Layout
+	command_pane.setBorder(BorderFactory.createEmptyBorder(2,0,2,0));
+	command_pane.setLayout(new BorderLayout());
+	command_pane.add(command_field, BorderLayout.CENTER);
+	command_pane.add(browse_button, BorderLayout.LINE_END);
+
+	lower_pane.setBorder(BorderFactory.createEmptyBorder(2,0,0,0));
+
+	button_pane.setLayout(new GridLayout(1,2,5,0));
+	button_pane.add(ok_button);
+	button_pane.add(cancel_button);
+
+	lower_pane.setBorder(BorderFactory.createEmptyBorder(2,0,0,0));
+	lower_pane.setLayout(new BorderLayout());
+	lower_pane.add(command_pane, BorderLayout.CENTER);
+	lower_pane.add(button_pane, BorderLayout.SOUTH);
+
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+        scrol_tmp=new JScrollPane(instructions_area);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.add(scrol_tmp, BorderLayout.NORTH);
+	content_pane.add(lower_pane, BorderLayout.CENTER);
+
+	Rectangle screen = Gatherer.g_man.getBounds(null);
+	setLocation((int)(screen.getX() + (screen.getWidth() - SIZE.width) / 2), (int)(screen.getY() + (screen.getHeight() - SIZE.height) / 2));
+	screen = null;
+    }
+
+    public void destroy() {
+	// Disconnect
+	// Clean up
+	self = null;
+    }
+
+    /** Redisplay the dialog
+     */
+    public String display() {
+	setVisible(true);
+	return preview_command;
+    }
+
+
+    private class OkButtonListener
+	implements ActionListener {
+	
+	public void actionPerformed(ActionEvent event) {
+	    preview_command = command_field.getText();
+	    self.dispose();
+	    
+	}
+    }
+
+
+    /** Whenever the user clicks the browse button, we should open up a file browser to allow them to select an executable file from somewhere in the file system. */
+    private class BrowseButtonListener 
+	implements ActionListener {
+	/** Open up a simple JFileChooser when the user clicks the button. 
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    JFileChooser chooser = new JFileChooser(new File(Gatherer.getGLIUserDirectoryPath()));
+            chooser.setComponentOrientation(Dictionary.getOrientation());
+	    GUIUtils.disableRename(chooser);
+	    chooser.setDialogTitle(Dictionary.get("FileAssociationDialog.Browse_Title"));
+	    chooser.setFileFilter(new BatchFileFilter());
+	    chooser.setFileFilter(new CoreObjectModelFileFilter());
+	    chooser.setFileFilter(new ExecutableFileFilter());
+	    chooser.setAcceptAllFileFilterUsed(true);
+	    if(chooser.showOpenDialog(Gatherer.g_man) == JFileChooser.APPROVE_OPTION) {
+		command_field.setText(chooser.getSelectedFile().getAbsolutePath());
+	    }
+	}
+    }
+
+    
+    private class CancelButtonListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    self.dispose();
+	}
+    }
+
+    /** Batch filter shows only files ending in bat. Based on ImageFilter.java which is a 1.4 example used by FileChooserDemo2.java. */
+    private class BatchFileFilter
+	extends FileFilter {
+
+	/** Accept all .exe files
+	 * @param file a File
+	 * @return true is this file should be shown, false otherwise
+	 */
+	public boolean accept(File file) {
+	    return file.getName().toLowerCase().endsWith(".bat");
+	}
+
+	/** The description of this filter
+	 * @return a String
+	 */
+	public String getDescription() {
+	    return Dictionary.get("FileAssociationDialog.Batch_File");
+	}
+    }
+
+    /** Command filter shows only files ending in com. Based on ImageFilter.java which is a 1.4 example used by FileChooserDemo2.java. */
+    private class CoreObjectModelFileFilter
+	extends FileFilter {
+
+	/** Accept all .com files
+	 * @param file a File
+	 * @return true is this file should be shown, false otherwise
+	 */
+	public boolean accept(File file) {
+	    return file.getName().toLowerCase().endsWith(".com");
+	}
+
+	/** The description of this filter
+	 * @return a String
+	 */
+	public String getDescription() {
+	    return Dictionary.get("FileAssociationDialog.Command_File");
+	}
+    }
+
+
+    /** Executable filter shows only files ending in exe. Based on ImageFilter.java which is a 1.4 example used by FileChooserDemo2.java. */
+    private class ExecutableFileFilter
+	extends FileFilter {
+
+	/** Accept all .exe files
+	 * @param file a File
+	 * @return true is this file should be shown, false otherwise
+	 */
+	public boolean accept(File file) {
+	    return file.getName().toLowerCase().endsWith(".exe");
+	}
+
+	/** The description of this filter
+	 * @return a String
+	 */
+	public String getDescription() {
+	    return Dictionary.get("FileAssociationDialog.Executable_File");
+	}
+    }
+
+
+}
+
+
+
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/RenamePrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/RenamePrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/RenamePrompt.java	(revision 31635)
@@ -0,0 +1,132 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2006 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+
+
+public class RenamePrompt
+    extends JDialog 
+    implements ActionListener, KeyListener
+{
+    private JButton cancel_button;
+    private JButton ok_button;
+    private JTextField name_textfield;
+    private String name;
+
+    static final private Dimension SIZE = new Dimension(350, 100);
+
+
+    public RenamePrompt(CollectionTreeNode collection_tree_node)
+    {
+	super(Gatherer.g_man, true);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	setTitle(Dictionary.get("RenamePrompt.Title"));
+	name = collection_tree_node.getFile().getName();
+    }
+
+
+    public void actionPerformed(ActionEvent event)
+    {
+	if (event.getSource() == ok_button) {
+	    name = name_textfield.getText();
+	}
+	else if (event.getSource() == cancel_button) {
+	    name = null;
+	}
+	dispose();
+    }
+
+    public void keyPressed(KeyEvent e) {
+	if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+            // Enter: Click the button in focus
+            Object source = e.getSource();
+            if (source instanceof AbstractButton) {
+                ((AbstractButton) source).doClick();
+            } else if (source == name_textfield) {
+		ok_button.doClick();
+	    }
+        }
+    }
+
+    public void keyReleased(KeyEvent e) { }
+    
+    public void keyTyped(KeyEvent e) { }
+
+
+    public String display()
+    {
+	setSize(SIZE);
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel info_pane = new JPanel();
+        info_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JLabel name_label = new JLabel(Dictionary.get("RenamePrompt.Name"));
+        name_label.setComponentOrientation(Dictionary.getOrientation());
+	name_textfield = new JTextField(name);
+        name_textfield.setComponentOrientation(Dictionary.getOrientation());
+
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Pure_Cancel_Tooltip"));
+	
+	// Connection
+	cancel_button.addActionListener(this);
+	ok_button.addActionListener(this);
+	ok_button.addKeyListener(this);
+	name_textfield.addKeyListener(this);
+
+	// Layout
+	info_pane.setLayout(new BorderLayout(5,0));
+	info_pane.add(name_label, BorderLayout.LINE_START);
+	info_pane.add(name_textfield, BorderLayout.CENTER);
+
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	button_pane.setLayout(new GridLayout(1,2,0,5));
+	button_pane.add(ok_button);
+	button_pane.add(cancel_button);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(info_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	// Display
+	Rectangle frame_bounds = Gatherer.g_man.getBounds();
+	setLocation(frame_bounds.x + (frame_bounds.width - SIZE.width) / 2, frame_bounds.y + (frame_bounds.height - SIZE.height) / 2);
+	setVisible(true);
+	return name;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ReplaceSrcDocWithHtmlPrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ReplaceSrcDocWithHtmlPrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/ReplaceSrcDocWithHtmlPrompt.java	(revision 31635)
@@ -0,0 +1,425 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Anupama Krishnan, Greenstone Digital Library, University of Waikato
+ * Code based entirely on Katherine Don's ExplodeMetadataDatabasePrompt.java
+ * (part of the Greenstone Digital Library)
+ *
+ * Copyright (C) 2008 Greenstone Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+import java.io.*;
+import java.util.ArrayList;
+
+import org.w3c.dom.Document;
+
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.Argument;
+import org.greenstone.gatherer.cdm.ArgumentControl;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.cdm.Plugin;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.collection.ScriptOptions;
+import org.greenstone.gatherer.collection.Collection;
+import org.greenstone.gatherer.feedback.Base64;
+import org.greenstone.gatherer.greenstone.LocalGreenstone;
+import org.greenstone.gatherer.gui.tree.DragTree;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.shell.GShellEvent;
+import org.greenstone.gatherer.shell.GShellListener;
+import org.greenstone.gatherer.util.Utility;
+import org.greenstone.gatherer.util.XMLTools;
+
+public class ReplaceSrcDocWithHtmlPrompt	
+    extends ModalDialog
+    implements GShellListener
+{
+    /** The size of this new collection dialog box. */
+    static private Dimension SIZE = new Dimension(675, 250);
+    private JDialog self;
+    
+    /** the source document files we wil be replacing with their GS3-generated html equivalent */
+    private File[] srcdoc_files = null;
+     /** the names of the srcdoc_files that could not be replaced with their GS3-generated html equivalent */
+    private java.util.Vector failedFiles = null;
+
+    /** the list of potential plugins to be used */
+    private Argument plugin_arg = null;
+    /** holds all the available options for the src-replacing script */
+    private ScriptOptions options = null;
+    /** the pane containing the options */
+    private JPanel options_pane = null;
+    /** the error message if any */
+    private StringBuffer error_message = null;
+    /** whether we were successful or not */
+    private boolean successful;
+    /** The command output of a successful execution of the replace script 
+     * containing the generated tailname of the replacement file */ 
+    private String successfulOutput;
+
+    public ReplaceSrcDocWithHtmlPrompt(File[] source_files) {
+	super(Gatherer.g_man, true);
+	this.self = this;
+	this.srcdoc_files = source_files;
+	failedFiles = new java.util.Vector(source_files.length); // number of failures will never be more than num source docs
+
+	// check that we actually have a src doc file which can be replaced with its GS3-generated html
+	// since all the files here have the same extension, the first file can be used to work out 
+	// the necessary plugin
+	ArrayList replacer_plugins = CollectionDesignManager.plugin_manager.getSrcReplacerPlugins(source_files[0]);
+	if (replacer_plugins.size() == 0) {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("ReplaceSrcWithHTMLPrompt.NotSrcReplaceable"), Dictionary.get("ReplaceSrcWithHTMLPrompt.Title"), JOptionPane.ERROR_MESSAGE);
+	    return;
+	}
+	plugin_arg = createPluginArgument(replacer_plugins);
+	setJMenuBar(new SimpleMenuBar("replacingSourceDoc"));
+	setSize(SIZE);
+	setTitle(Dictionary.get("ReplaceSrcWithHTMLPrompt.Title"));
+	
+	// set up the script options
+	// we have empty initial values
+	String dom_string = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Options/>";
+	Document doc = XMLTools.parseXML(new StringReader(dom_string));
+	options = new ScriptOptions(doc.getDocumentElement(), "replace_srcdoc_with_html.pl");
+	
+	// Creation
+	JPanel content_pane = (JPanel) getContentPane();
+	content_pane.setOpaque(true);
+	content_pane.setComponentOrientation(Dictionary.getOrientation());        
+	options_pane = new JPanel();
+        
+	addScriptOptions(options_pane);
+	JScrollPane middle_pane = new JScrollPane(options_pane);
+        middle_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JTextArea instructions_area = new JTextArea(Dictionary.get("ReplaceSrcWithHTMLPrompt.Instructions"));
+	instructions_area.setComponentOrientation(Dictionary.getOrientation());
+        instructions_area.setEditable(false);
+	instructions_area.setLineWrap(true);
+	instructions_area.setRows(5);
+	instructions_area.setWrapStyleWord(true);
+	
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	JButton srcreplace_button = new GLIButton(Dictionary.get("ReplaceSrcWithHTMLPrompt.ReplaceSrc"), Dictionary.get("ReplaceSrcWithHTMLPrompt.ReplaceSrc_Tooltip"));
+	JButton cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Cancel_Tooltip"));
+	
+	// Connection
+	cancel_button.addActionListener(new CancelListener());
+	srcreplace_button.addActionListener(new SrcReplaceListener());
+
+	// Layout
+
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	button_pane.setLayout(new GridLayout(1,2));
+	button_pane.add(srcreplace_button);
+	button_pane.add(cancel_button);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(instructions_area, BorderLayout.NORTH);
+	content_pane.add(middle_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	// Final dialog setup & positioning.
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(true);
+    }
+
+    public void destroy() 
+    {
+       	
+    }
+
+    /** All implementation of GShellListener must include this method so the listener can be informed of messages from the GShell.
+     * @param event A <strong>GShellEvent</strong> that contains, amoung other things, the message.
+     */
+    public synchronized void message(GShellEvent event) {
+	String message = event.getMessage();
+	if (message.startsWith("replace_srcdoc_with_html.pl>")) {
+	    message = message.substring(28); // length of "replace_srcdoc_with_html.pl>"?
+	    error_message.append(message);
+	    error_message.append("\n");
+	}
+    }
+
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell begins its task. Implementation side-effect, not actually used.
+     * @param event A <strong>GShellEvent</strong> that contains details of the initial state of the <strong>GShell</strong> before task comencement.
+     */
+    public synchronized void processBegun(GShellEvent event) {
+	// We don't care. 
+    }
+    
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell completes its task.
+     * @param event A <strong>GShellEvent</strong> that contains details of the final state of the <strong>GShell</strong> after task completion.
+     */
+    public synchronized void processComplete(GShellEvent event) {
+	successful = false;
+	if(event.getStatus() == GShell.OK) {
+	    successful = true;
+	    GShell process = (GShell)event.getSource();
+	    successfulOutput = process.getCommandOutput();
+	    process.resetCommandOutput();
+	}
+    }
+
+    private Argument createPluginArgument(ArrayList plugin_list) {
+	Argument arg = new Argument();
+	arg.setName("plugin");
+	arg.setDescription(Dictionary.get("ReplaceSrcWithHTMLPrompt.Plugin"));
+	arg.setRequired(true);
+	arg.setType(Argument.ENUM);
+	for (int i=0; i<plugin_list.size(); i++) {
+	    Plugin p = (Plugin)plugin_list.get(i);
+	    arg.addOption(p.getName(), p.getName());
+	}
+	return arg;
+    }
+    private void addScriptOptions(JPanel options_pane) {
+
+	options_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	options_pane.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+	options_pane.setLayout(new BoxLayout(options_pane, BoxLayout.Y_AXIS));
+
+	int current_mode = Configuration.getMode();
+	int total_argument_count = options.getArgumentCount();
+       
+	// first we add the plugin arg
+	ArgumentControl argument_control = new ArgumentControl(plugin_arg, true, null);
+	argument_control.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+	options_pane.add((JComponent)argument_control);
+	for(int i = 0; i < total_argument_count; i++) {
+	    // Retrieve the argument so we know how to format the control.
+	    Argument argument = options.getArgument(i);
+	    if(!argument.isHiddenGLI() && argument.getModeLevel() <= current_mode) {
+		// by default, all args are disabled, and no value
+		argument_control = new ArgumentControl(argument, false, null);
+	
+		// make sure they are coloured the way we want - this is not the standard arg control coloring
+		argument_control.setBackground(Configuration.getColor("coloring.collection_heading_background", false));
+		options_pane.add((JComponent)argument_control);
+	    }
+	}
+    }
+
+    private void updateScriptOptions() {
+	for(int i = 0; i < options_pane.getComponentCount(); i++) {
+	    Component component = options_pane.getComponent(i);
+	    if(component instanceof ArgumentControl) {
+		ArgumentControl ac = (ArgumentControl)component;
+		String name = ac.getArgumentName();
+		String value = ac.getValue();
+		boolean enabled = ac.isEnabled();
+		if (!enabled && value==null) {
+		    // flag type, remove from options altogether
+		    options.removeValue(name);
+		} else {
+		    if (value.equals("")) {
+			// if the value is empty, we don't want it to be passed to the script
+			options.setValue(name, false, value);
+		    } else {
+			options.setValue(name, enabled, value);
+		    }
+		}
+
+	    }
+	}
+    }
+  
+    private int replaceSrcDoc(int fileNum)
+    {
+	// Generate the replace_srcdoc_with_html.pl command
+	ArrayList command_parts_list = new ArrayList();
+	if (!Gatherer.isGsdlRemote) {
+	    command_parts_list.add(Configuration.perl_path);
+	    command_parts_list.add("-S");
+	}
+	command_parts_list.add(LocalGreenstone.getBinScriptDirectoryPath() + "replace_srcdoc_with_html.pl");
+	
+	// Add in all the options from the user
+	String[] src_replace_options = options.getValues();
+	for (int i = 0; i < src_replace_options.length; i++) {
+	    command_parts_list.add(src_replace_options[i]);
+	}
+
+	// Local case
+	if (!Gatherer.isGsdlRemote) {    
+	    // Add in the filename
+	    command_parts_list.add(this.srcdoc_files[fileNum].getPath());
+	}
+	// Remote case
+	else {
+	    // Add in the filename, relative to the collection directory
+	    String collection_name = CollectionManager.getLoadedCollectionName();
+	    String collection_directory_path = CollectionManager.getCollectionDirectoryPath(collection_name);
+	    String srcdoc_file_relative_path = Gatherer.remoteGreenstoneServer.getPathRelativeToDirectory(
+		this.srcdoc_files[fileNum], collection_directory_path); // preserves spaces in filename
+	    srcdoc_file_relative_path = srcdoc_file_relative_path.replace(File.separatorChar, '|');
+
+	    // base64 encode the relative filepath, so that special characters in the filename are preserved
+	    command_parts_list.add("-file");
+	    command_parts_list.add(Base64.encodeBytes(srcdoc_file_relative_path.getBytes()));
+	    	    
+	    // When running remotely we also need the collection name as the last argument
+	    command_parts_list.add(collection_name);
+	}
+
+	// Run the replace_srcdoc_with_html.pl command
+	String[] command_parts = (String[]) command_parts_list.toArray(new String[0]);
+	this.error_message = new StringBuffer();
+
+	GShell process = new GShell(command_parts, GShell.SRCREPLACE, 3, this, null, GShell.GSHELL_SRCREPLACE);
+	//process.start();
+	process.run();
+	
+	if (successful) {
+	    return 0;
+	} 
+	return 1;
+    }	
+
+    private void resultPrompt(boolean success, String message)
+    {
+	if (success) {
+	    // !!! TO DO: Get replace_srcdoc_with_html.pl strings translated and use SimpleResultDialog below
+	    JOptionPane.showMessageDialog(null, Dictionary.get("ReplaceSrcWithHTMLPrompt.Successful_ReplaceSrc"), Dictionary.get("ReplaceSrcWithHTMLPrompt.Successful_Title"), JOptionPane.INFORMATION_MESSAGE);
+	}
+	else {
+	    String title = Dictionary.get("ReplaceSrcWithHTMLPrompt.Failed_Title");
+	    
+	    // comma separated list of all the names of the files that could not be replaced
+	    String failedFilenames = failedFiles.size() > 0 ? failedFiles.get(0).toString() : "";
+	    for(int i = 1; i < failedFiles.size(); i++) {
+		failedFilenames = failedFilenames + ", " + failedFiles.get(i);
+	    }
+
+	    String label = Dictionary.get("ReplaceSrcWithHTMLPrompt.Failed_ReplaceSrc", failedFilenames);//srcdoc_file.getName());
+	    SimpleResultDialog result_dialog = new SimpleResultDialog(title, label, message);
+	    result_dialog.setVisible(true); // Blocks
+	    result_dialog.dispose();
+	    result_dialog = null;
+	}
+    }
+
+    private class CancelListener
+	implements ActionListener {
+	public void actionPerformed(ActionEvent event) {
+	    self.dispose();
+	}
+    }
+    
+    private class SrcReplaceListener
+	implements ActionListener {
+	
+	public void actionPerformed(ActionEvent event) {
+	    Gatherer.g_man.wait(true);
+	    // update the options
+	    updateScriptOptions();
+	    self.dispose();  
+	    (new ReplaceSrcDocWithHtmlTask()).start();
+	}
+    }
+
+
+    private class ReplaceSrcDocWithHtmlTask
+	extends Thread
+    {
+	public ReplaceSrcDocWithHtmlTask()
+	{
+	}
+
+	public void run()
+	{
+	    int exit_value = 0;
+	    String errMsg = "";
+
+	    // We're going to try replacing all selected documents and then 
+	    // display errormessages
+	    // first run the replace process for all src files, and accumulate error exit values (0 means it's fine)
+	    for(int i = 0; i < srcdoc_files.length; i++) {
+		int exit_value_this_time = replaceSrcDoc(i);
+		exit_value += exit_value_this_time; // if all files successfully replaced, exit_value will stay at 0
+
+		// accumulate all error and success msgs to display after processing all the files
+		// that are to be replaced
+		errMsg += error_message.toString();
+		error_message = null;
+
+		if(exit_value_this_time != 0) {
+		    failedFiles.add(srcdoc_files[i].getName()); // add this file to list of failures
+		} else {
+		    if (Gatherer.isGsdlRemote) {
+			//System.err.println("*****ReplaceSrcDocWithHtmlPrompt.java - run() - gsdl remote case");
+
+			// Conversion may have renamed the file by URL- or base64-encoding it.
+			// The new replacement file's tailname is the last line of the script output.
+			String[] lines = successfulOutput.split("\n");
+			String suffixlessFilename = lines[lines.length-1];
+			String htmlFile = suffixlessFilename+".html";
+
+			// Delete the local copy of the old source document file on the client side 
+			// (it's already been replaced on the server side), and download the updated 
+			// (html) file and any directory containing associated files
+			Utility.delete(srcdoc_files[i]); // remove the local copy of src doc
+
+			// download the generated html file from the server side to put it 
+			// into the import directory on the client side
+			Gatherer.remoteGreenstoneServer.downloadCollectionFile(
+					      CollectionManager.getLoadedCollectionName(),
+					      new File(srcdoc_files[i].getParentFile(), htmlFile));
+			
+			// download any assoc folder which is srcfilename+"_files"
+			File assoc_folder = new File(srcdoc_files[i].getParentFile(), suffixlessFilename+"_files");
+			
+			// If an associated_folder by such a name exists, download it
+			if(Gatherer.remoteGreenstoneServer.exists(CollectionManager.getLoadedCollectionName(), assoc_folder)) {
+			    Gatherer.remoteGreenstoneServer.downloadCollectionFile(
+			                        CollectionManager.getLoadedCollectionName(), assoc_folder);
+			}
+		    }
+		    // Whether remote or local case, need to refressh the GLI filemanager system to show
+		    // what files are in the current collection space
+		    Gatherer.g_man.refreshCollectionTree(DragTree.COLLECTION_CONTENTS_CHANGED); // refreshes coll's content view
+		    Gatherer.g_man.wait(false);
+		}
+	    } // end for loop
+	    
+	    // Display error messages now that we finished processing the multiple files
+	    if(exit_value != 0) {
+		resultPrompt(false, errMsg);
+	    } else {
+		resultPrompt(true, errMsg);
+	    }
+	} // end run method
+
+    }    //end Task class
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/SimpleMenuBar.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/SimpleMenuBar.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/SimpleMenuBar.java	(revision 31635)
@@ -0,0 +1,36 @@
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.util.JarTools;
+
+public class SimpleMenuBar
+    extends JMenuBar
+    implements ActionListener {
+
+    private String page_name;
+
+    public SimpleMenuBar(String page_name) {
+	this.page_name = page_name;
+        this.setComponentOrientation(Dictionary.getOrientation());
+	JMenu help = new JMenu();
+        help.setComponentOrientation(Dictionary.getOrientation());
+	help.setIcon(JarTools.getImage("help.gif"));
+	help.setText(Dictionary.get("Menu.Help"));
+
+	JMenuItem help_help = new JMenuItem(Dictionary.get("Menu.Help"));
+        help_help.setComponentOrientation(Dictionary.getOrientation());
+	help_help.addActionListener(this);
+	
+	help.add(help_help);
+
+	add(Box.createHorizontalGlue());
+	add(help);
+    }
+
+    public void actionPerformed(ActionEvent event) {
+	HelpFrame.setView(page_name);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/SimpleResultDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/SimpleResultDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/SimpleResultDialog.java	(revision 31635)
@@ -0,0 +1,125 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.ArrayList;
+import javax.swing.*;
+import javax.swing.event.*;
+
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.Dictionary;
+
+public class SimpleResultDialog
+  extends ModalDialog {
+    
+  private Dimension size = new Dimension(600,300);
+  private SimpleResultDialog self;
+    
+  public SimpleResultDialog(String title, String label, String results) {
+    super(Gatherer.g_man, title, true);
+    this.self = this;
+	this.setComponentOrientation(Dictionary.getOrientation());
+        
+    setUpGUI(label, results);
+  }
+
+  /** If you are calling this from another dialog, and you want that dialog blocked while this is open, need to use this method, passing in the calling dialog as the parent */
+  public SimpleResultDialog(Dialog parent, String title, String label, String results) {
+
+    super(parent, title, true);
+    this.self = this;
+  
+    setUpGUI(label, results);
+  }
+
+  private void setUpGUI(String label, String results) {
+    setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+    setSize(size);
+	
+    JPanel content_pane = (JPanel) getContentPane();
+    content_pane.setComponentOrientation(Dictionary.getOrientation());
+    JLabel result_label = new JLabel(label);
+    result_label.setComponentOrientation(Dictionary.getOrientation());
+	
+    JPanel button_pane = new JPanel();
+    button_pane.setComponentOrientation(Dictionary.getOrientation());
+    JButton close_button = new GLIButton(Dictionary.get("General.Close"));
+    close_button.addActionListener(new CloseButtonListener());
+	
+    JPanel output_pane = new JPanel();
+    output_pane.setComponentOrientation(Dictionary.getOrientation());
+    JLabel output_label = new JLabel(Dictionary.get("General.Review_Output"));
+    output_label.setComponentOrientation(Dictionary.getOrientation());
+
+    JTextArea output_textarea = new JTextArea(results);
+    output_textarea.setComponentOrientation(Dictionary.getOrientation());
+    output_textarea.setCaretPosition(0);
+    output_textarea.setEditable(false);
+    output_textarea.setLineWrap(true);
+    output_textarea.setWrapStyleWord(true);
+	
+    button_pane.setLayout(new BorderLayout());
+    button_pane.add(close_button, BorderLayout.LINE_END);
+	
+    output_pane.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));
+    output_pane.setLayout(new BorderLayout());
+    output_pane.add(output_label, BorderLayout.NORTH);
+    output_pane.add(new JScrollPane(output_textarea), BorderLayout.CENTER);
+	
+    content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+    content_pane.setLayout(new BorderLayout());
+    content_pane.add(result_label, BorderLayout.NORTH);
+    content_pane.add(output_pane, BorderLayout.CENTER);
+    content_pane.add(button_pane, BorderLayout.SOUTH);
+	
+    // Position
+    Rectangle frame_bounds = Gatherer.g_man.getBounds();
+    setLocation(frame_bounds.x + (frame_bounds.width - size.width) / 2, frame_bounds.y + (frame_bounds.height - size.height) / 2);
+    //new SimpleResultDialog(Gatherer.g_man, title, label, results);
+  }
+  
+  
+  private class CloseButtonListener
+    implements ActionListener {
+    public void actionPerformed(ActionEvent event) {
+      self.setVisible(false);
+    }
+  }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/URLField.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/URLField.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/URLField.java	(revision 31635)
@@ -0,0 +1,316 @@
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.net.*;
+import javax.swing.*;
+import java.util.*;
+import java.io.*;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.Configuration;
+
+/** Modified. 
+ * Class used by WarningDialog and Gatherer which represents a textual input control 
+ * (TextField or Combobox) into which a URL can be entered. Provides some static methods
+ * shared by these controls and by a general JTextField control which can all be used 
+ * in a WarningDialog. Class URLField also contains an inner Interface class and two 
+ * static inner classes that implement this interface: Text and DropDown. 
+ * Finally, another static inner class URLCollectionPair associates each Gliserver URL
+ * (or Library URL) that can be displayed in the DropDown with the last opened collection
+ * file for the Greenstone server at that URL. 
+*/
+public class URLField
+{
+    static public String getText(JComponent control)
+    {
+	if(control instanceof JTextField) { // not specifically a URL Field, therefore, we're not meant to do checking
+	    return ((JTextField)control).getText();
+	} 
+	else if(control instanceof URLField.DropDown) {
+	    return (String)((JComboBox)control).getSelectedItem();
+	}
+
+	// unknown control, shouldn't happen
+	return "";
+    }
+
+    static public String setText(JComponent control, String text)
+    {
+	if(control instanceof JTextField) { // not specifically a URL Field, therefore, we're not meant to do checking
+	    ((JTextField)control).setText(text);
+	} 
+	else if(control instanceof URLField.DropDown) {
+	    ((JComboBox)control).setSelectedItem(text);	    
+	}
+
+	// unknown control, shouldn't happen
+	return "";
+    }
+
+    static public boolean validateURL(JComponent control)
+    {
+	if(control instanceof URLField.Text) {
+	    return ((URLField.Text)control).validateURL();
+	} 
+	else if (control instanceof JTextField) {
+	    return true; // not specifically a URL Field, therefore, we're not meant to do checking
+	}
+	else { // control is a DropDown
+	    return ((URLField.DropDown)control).validateURL();
+	}
+    }
+
+    static public boolean validateURL(String url_string)
+    {
+	if (!url_string.equals("")) {
+	    // Check the URL string is valid by trying to create a URL object from it
+	    try {
+		new URL(url_string);
+	    }
+	    catch (MalformedURLException exception) {
+		// URL string is invalid
+		return false;
+	    }
+	}
+
+	// URL string is valid
+	return true;
+    }
+
+    static public void store(JComponent control, String affected_property) {
+	if(control instanceof JTextField) { 
+	    String value = ((JTextField)control).getText();
+	    Configuration.setString(affected_property, true, value);
+	    Configuration.save(); // save it in case of a crash
+	} 
+	else { // DropDown
+	    ((DropDown)control).saveProperties(); // saves it already
+	}
+    }
+
+    
+    /** INTERFACE CLASS IMPLEMENTED BY Static inner classes Text and DropDown */
+    public static interface URLFieldControl {
+	public boolean validateURL();
+    }
+
+
+    /** STATIC INNER CLASS URLTextField */
+    public static class Text 
+	extends JTextField implements URLFieldControl 
+    {
+	public Text(Color foreground, Color background)
+	{
+	    super();
+	     this.setComponentOrientation(Dictionary.getOrientation());
+	    setBackground(background);
+	    setForeground(foreground);
+	}
+
+	public boolean validateURL() {
+	    String url_string = getText();
+	    return URLField.validateURL(url_string);
+	}
+    }
+
+   
+    /** STATIC INNER CLASS DropDown, an editable URLComboBox */
+    public static class DropDown 
+	extends JComboBox implements URLFieldControl
+    {
+	private String checkString = null; // optional string to check the URLs items against to see whether the URLs contain this
+	private String affectedProperty = null; // the Configuration property associated with this DropDown
+	private String coaffectedProperty = null; // the Configuration property whose suffix should change along with the affectedProperty
+	
+	private static final int MAX_URLS = 5;
+
+	public DropDown(Color foreground, Color background, String[] defaultURLs, 
+			String affectedProperty, String coaffectedProperty)
+	{
+	    super(URLCollectionPair.createDefaultsArray(defaultURLs));
+	    this.setEditable(true);
+	    this.getEditor().selectAll();
+	    this.setComponentOrientation(Dictionary.getOrientation());
+
+	    setBackground(background);
+	    setForeground(foreground);
+	    
+	    this.affectedProperty = affectedProperty;
+	    this.coaffectedProperty = coaffectedProperty;
+	    // read any URLS specified in the config file
+	    setComboBoxValues();
+	    
+	}
+
+	public DropDown(Color foreground, Color background, String[] defaultURLs, 
+			String affectedProperty, String coaffectedProperty, String checkString)
+	{
+	    this(foreground, background, defaultURLs, affectedProperty, coaffectedProperty);
+	    this.checkString = checkString;
+	}	
+
+	public boolean validateURL() {
+	    //String url_string = (String)this.getEditor().getItem();
+	    String url_string = this.getSelectedItem().toString(); // returns url of the combobox item
+		
+	    if(checkString != null && !url_string.endsWith(checkString)) {
+		return false;
+	    } else {
+		return URLField.validateURL(url_string);
+	    }
+	}
+
+	public void saveProperties() {
+	    // 1. Add the first url just edited (if any) 
+
+	    // The user might have created a new STRING, or selected an existing URLCollectionPair
+	    // We don't know the type!
+	    Object o = this.getSelectedItem(); 
+	    URLCollectionPair editedItem = (o instanceof URLCollectionPair) 
+		? (URLCollectionPair)o : new URLCollectionPair(o.toString());
+	    	    
+	    if(editedItem != null) {
+		insertItemAt(editedItem, 0);
+	    } else {
+		editedItem = (URLCollectionPair)getItemAt(0); // else reuse the first default:
+	    }
+	    Configuration.setString(affectedProperty, true, editedItem.getURL());
+	    
+	    // concurrently set the last opened collection for this url
+	    String lastOpenedCollection = editedItem.getCollection();
+	    if(lastOpenedCollection.equals("")) {
+		Configuration.setString(coaffectedProperty, true, null);
+	    } else {
+		Configuration.setString(coaffectedProperty, true, lastOpenedCollection);
+	    }
+
+
+	    // 2. Add the remaining urls into the combobox
+	    int url_count = 1; // remain under MAX_URLS AND under the number of actual URLs in the dropdown
+	    for(int i = 0; url_count < MAX_URLS && i < getItemCount(); i++, url_count++) {
+		
+		URLCollectionPair item = (URLCollectionPair)getItemAt(i);
+		String url = item.getURL();
+		
+		if(item == null || url.equals(editedItem.getURL()) || url.equals("")) { 
+		    // skip any duplicates of the just-edited URL and skip empty URLs
+		    url_count--;
+		} 
+		else { 
+		    Configuration.setString(affectedProperty+url_count, true, url);
+		    
+		    // concurrently save the name of the corresponding open collection
+		    if(item.getCollection().equals("")) {
+			Configuration.setString(coaffectedProperty+url_count, true, null);
+		    } else {
+			Configuration.setString(coaffectedProperty+url_count, true, item.getCollection());
+		    }
+
+		} // else retain old value for this affectedProperty (general.gliserver_url)
+	    }
+	    Configuration.save();
+	}
+
+	
+	// Get values from Configuration file, or (if none) use defaultURLs.
+	// Put them into the combobox
+	private void setComboBoxValues() {
+
+	    String url = Configuration.getString(affectedProperty, true);
+	    String collection = Configuration.getString(coaffectedProperty, true);
+	    boolean finished = url.equals("");
+	    if(!finished) {
+		this.removeAllItems(); // remove defaults, since config file now contains init values
+		this.addItem(new URLCollectionPair(url, collection));
+	    } // else urls and combobox already contains the defaults
+	   	    
+
+	    for(int i = 1; !finished; i++) {
+		url = Configuration.getString(affectedProperty+i, true);
+		collection = Configuration.getString(coaffectedProperty+i, true);
+		if(url.equals("")) {
+		    finished = true;
+		}
+		else { // url is not empty
+		    this.addItem(new URLCollectionPair(url, collection));
+		}
+	    }
+	    
+	    // setting the size of the dropdown control
+	    Dimension newSize=new Dimension(getPreferredSize().width+20, getPreferredSize().height);
+	    setPreferredSize(newSize);
+	}
+    }
+
+
+    /** STATIC INNER CLASS URLCollectionPair. 
+     * Since the configuration for a (remote) Greenstone2 and Greenstone3 is now
+     * stored in a single file for both, we can have GLI connecting to different servers 
+     * while for each server, a different collection may have been left open last time.
+     * This class URLCollectionPair will associate a GLIServer_URL or Library_URL with
+     * the last opened collection file for it. It is used by URLField.DropDown combobox. */
+    public static class URLCollectionPair {
+	String url;        // gliserverURL 
+	String collection; // last opened collection for the corresponding gliserver
+
+	
+	public static URLCollectionPair[] createDefaultsArray(String[] defaultURLs) {
+	    URLCollectionPair[] pairs = new URLCollectionPair[defaultURLs.length];
+	    for(int i = 0; i < pairs.length; i++) {
+		pairs[i] = new URLCollectionPair(defaultURLs[i]); // no associated open collection
+	    }
+	    return pairs;
+	}
+
+	public URLCollectionPair(String url) {
+	    this.set(url, "");
+	}
+
+	public URLCollectionPair(String url, String collection) {
+	    this.set(url, collection);
+	}
+
+	public void set(String url, String collection) {
+	    this.url = url;
+	    this.collection = collection;
+	}
+
+	public void setURL(String url) {
+	    this.url = url;
+	}
+
+	public void setCollection(String collection) {
+	    this.collection = collection;
+	}
+
+	public String getCollection() {
+	    return collection;
+	}
+
+	public String getURL() {
+	    return url;
+	}
+
+	/** For display in comboboxes */
+	public String toString() {
+	    return url;
+	}
+
+	/** @returns the number that is suffixed to the gliserver_url */
+	public String getSuffixNumber() {
+	    String numericSuffix = "";
+	    boolean stop = false;
+	    for(int index = url.length()-1; index >= 0 && !stop; index--) { // work from the end
+		char character = url.charAt(index);
+		if(Character.isDigit(character)) {
+		    numericSuffix = Character.toString(character) + numericSuffix;
+		} else {
+		    stop = true;
+		}
+	    }
+
+	    return numericSuffix;
+	}
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/WarningDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/WarningDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/WarningDialog.java	(revision 31635)
@@ -0,0 +1,288 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Project, NZDL, University of Waikato
+ *
+ * Copyright (C) 2003 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.JarTools;
+
+
+/** A dialog that warns about something, and allows the user to turn off the warning in the future. */
+public class WarningDialog
+    extends ModalDialog 
+    implements ActionListener, KeyListener
+{
+    static final private Dimension NORMAL_SIZE = new Dimension(450, 160);
+    static final private Dimension SETTING_SIZE = new Dimension(450, 200);
+
+    private int result = JOptionPane.CANCEL_OPTION;
+    private JButton cancel_button;
+    private JButton ok_button;
+    private JCheckBox show_check;
+    private JComponent value_field;
+    private JPanel value_panel;
+    private String affected_property;
+    private String full_property;
+    /** Whether or not to display the checkbox that will turn off this dialog in future */
+    private final boolean showcheckbox;
+
+
+    public WarningDialog(String warning_name, String warning_title, String warning_message, String affected_property, boolean can_cancel) 
+    {
+	this(warning_name, warning_title, warning_message, affected_property, can_cancel, true); // true for show checkbox
+    }
+
+
+    public WarningDialog(String warning_name, String warning_title, String warning_message, String affected_property, boolean can_cancel,
+			 boolean showcheckbox)
+    {
+	super(Gatherer.g_man, "Warning", true);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	this.showcheckbox = showcheckbox; 
+
+	// Determine the name of this prompt.
+	this.affected_property = affected_property;
+	this.full_property = warning_name;
+
+	// Now build dialog.
+	if (affected_property != null) {
+	    setSize(SETTING_SIZE);
+	}
+	else {
+	    setSize(NORMAL_SIZE);
+	}
+	setTitle(warning_title);
+	
+	// Creation
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel text_pane = new JPanel();
+        text_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	String gmedium_image = "gatherer_medium.png";
+	if (Configuration.fedora_info.isActive()) {
+	    gmedium_image = "fli-" + gmedium_image;
+	}
+
+	JLabel icon_label = new JLabel(JarTools.getImage(gmedium_image));
+        icon_label.setComponentOrientation(Dictionary.getOrientation());
+
+	JTextArea text_area = new JTextArea();
+        text_area.setComponentOrientation(Dictionary.getOrientation());
+	text_area.setEditable(false);
+	text_area.setLineWrap(true);
+	text_area.setText(warning_message);
+	text_area.setCaretPosition(0);
+	text_area.setWrapStyleWord(true);
+
+	value_panel = new JPanel();
+        value_panel.setComponentOrientation(Dictionary.getOrientation());
+	JLabel value_label = new JLabel(Dictionary.get("WarningDialog.Value"));
+	value_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	value_field = new JTextField();
+        value_field.setComponentOrientation(Dictionary.getOrientation());
+	JPanel bottom_pane = new JPanel();
+        bottom_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JPanel control_pane = new JPanel();
+        control_pane.setComponentOrientation(Dictionary.getOrientation());
+	ok_button = new GLIButton(Dictionary.get("General.OK"), Dictionary.get("General.OK_Tooltip"));
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Pure_Cancel_Tooltip"));
+	
+	// Connection
+	ok_button.addActionListener(this);
+	cancel_button.addActionListener(this);
+	ok_button.addKeyListener(this);
+	cancel_button.addKeyListener(this);
+	getRootPane().setDefaultButton(ok_button);
+
+	// Layout
+	icon_label.setBorder(BorderFactory.createEmptyBorder(0,0,0,5));
+
+	value_label.setBorder(BorderFactory.createEmptyBorder(0, icon_label.getPreferredSize().width, 0, 0));
+
+	value_panel.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	value_panel.setLayout(new BorderLayout(5,0));
+	value_panel.add(value_label, BorderLayout.LINE_START);
+	value_panel.add(value_field, BorderLayout.CENTER);
+
+	text_pane.setLayout(new BorderLayout());
+	text_pane.add(icon_label, BorderLayout.LINE_START);
+	text_pane.add(new JScrollPane(text_area), BorderLayout.CENTER);
+	if(affected_property != null) {
+	    text_pane.add(value_panel, BorderLayout.SOUTH);
+	}
+
+	if(can_cancel) {
+	    control_pane.setLayout(new GridLayout(1,2,5,0));
+	    control_pane.add(ok_button);
+	    control_pane.add(cancel_button);
+	}
+	else {
+	    control_pane.setLayout(new BorderLayout());
+	    control_pane.add(ok_button, BorderLayout.LINE_END);
+	}
+
+	bottom_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	bottom_pane.setLayout(new BorderLayout());
+	bottom_pane.add(control_pane, BorderLayout.LINE_END);
+	if(showcheckbox) {
+	    show_check = new JCheckBox(Dictionary.get("WarningDialog.Dont_Show_Again"));
+	    show_check.setComponentOrientation(Dictionary.getOrientation());
+	    bottom_pane.add(show_check, BorderLayout.CENTER);
+	}
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(text_pane, BorderLayout.CENTER);
+	content_pane.add(bottom_pane, BorderLayout.SOUTH);
+
+	// Position
+	Dimension size = getSize();
+	if (Gatherer.g_man != null) {
+	    Rectangle frame_bounds = Gatherer.g_man.getBounds();
+	    setLocation(frame_bounds.x + (frame_bounds.width - size.width) / 2, frame_bounds.y + (frame_bounds.height - size.height) / 2);
+	}
+	else {
+	    Dimension screen_size = Toolkit.getDefaultToolkit().getScreenSize();
+	    setLocation((screen_size.width - size.width) / 2, (screen_size.height - size.height) / 2);
+	}
+    }
+
+    public void actionPerformed(ActionEvent event) {
+	boolean valid_value = true;
+	if(event.getSource() == ok_button) {
+	    if(affected_property != null && Configuration.self != null) {
+		valid_value = URLField.validateURL(value_field);
+		if(valid_value) {
+		    // Store the value of the property
+		    URLField.store(value_field, affected_property); //Configuration.setString(affected_property, true, value);
+		}
+	    }
+	    if(valid_value) {
+		result = JOptionPane.OK_OPTION;
+	    }
+	} 
+
+	if(valid_value) {
+	    if (Configuration.self != null && showcheckbox) {
+		// Store the state of the show message checkbox.
+		Configuration.set(full_property, true, !show_check.isSelected());
+	    }
+	    // Done.
+	    setVisible(false);
+	}
+	else {
+	    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("WarningDialog.Invalid_Value"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	}
+    }
+
+    /** Gives notification of key events on the buttons */
+    public void keyPressed(KeyEvent e) {
+	if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+	    // Enter: Click the button in focus
+	    Object source = e.getSource();
+	    if (source instanceof AbstractButton) {
+		((AbstractButton) source).doClick();
+	    }
+	}
+    }
+
+    public void keyReleased(KeyEvent e) {}
+
+    public void keyTyped(KeyEvent e) {}
+
+
+    public int display() {
+	if (Configuration.self == null || Configuration.get(full_property, false)) {
+	    setVisible(true);
+	}
+	// We are no longer showing this dialog, so result must always be true.
+	else {
+	    result = JOptionPane.OK_OPTION;
+	}
+	// Ask the parent to repaint, just to be sure
+	if(Gatherer.g_man != null) {
+	    Gatherer.g_man.repaint();
+	}
+	return result;
+    }
+
+    /** Allows you to specify whether this dialog is a warning dialog, or only a message dialog.
+     * @param message_only true if this dialog shows a message, false for a warning
+     */
+    public void setMessageOnly(boolean message_only) {
+	if(!showcheckbox) {
+	    return;
+	}
+
+	// If this is a message then change the checkbox
+	if(message_only) {
+	    show_check.setText(Dictionary.get("WarningDialog.Dont_Show_Again_Message"));
+	}
+	// And if its a warning, change them back
+	else {
+	    show_check.setText(Dictionary.get("WarningDialog.Dont_Show_Again"));
+	}
+    }
+
+    /** Allows you to replace the generic text field control with a JTextField subclass with further functionality. For instance you might provide a URLField to allow only valid URLs to be accepted.
+     * @param  control the JTextField subclass you want to use for the control
+     */
+    public void setValueField(JComponent control) {
+	// Remove the current control
+	value_panel.remove(value_field);
+	// Replace with the new one
+	value_field = control;
+	// Re-add
+	value_panel.add(value_field, BorderLayout.CENTER);
+    }
+
+    protected void processWindowEvent(WindowEvent event) {
+	if(event.getID() == WindowEvent.WINDOW_ACTIVATED) {
+	    if(affected_property != null) {
+		value_field.requestFocus();
+	    }
+	    else {
+		ok_button.requestFocus();
+	    }
+	}
+	else {
+	    super.processWindowEvent(event);
+	}
+    }
+
+    public void setValueField(String field_value){
+	URLField.setText(this.value_field, field_value);
+    }
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/WriteCDImagePrompt.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/WriteCDImagePrompt.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/WriteCDImagePrompt.java	(revision 31635)
@@ -0,0 +1,661 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.ArrayList;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.BasicCollectionConfiguration;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.greenstone.LocalGreenstone;
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.shell.GShellEvent;
+import org.greenstone.gatherer.shell.GShellListener;
+import org.greenstone.gatherer.util.ArrayTools;
+import org.greenstone.gatherer.util.CheckList;
+import org.greenstone.gatherer.util.CheckListEntry;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+/** This class provides the functionality to write a CD-ROM/DVD image of current collections from the GSDLHOME/collect/ directory to CDROM. The user chooses the collection from a list, where each entry also displays details about itself, confirms the delete of a collection by checking a checkbox then presses the ok button to actually delete the collection.
+    Copied from DeleteCollectionPrompt
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class WriteCDImagePrompt
+    extends ModalDialog 
+    implements GShellListener {
+    
+    private OKButtonListener ok_button_listener;
+    private ArrayList all_collections = null;
+    private ArrayList selected_collections = null;
+    /** The list of collections to include in exported cd-rom/dvd image */
+    private CheckList list = null;
+    /** The currently selected collection for deletion. */
+    private BasicCollectionConfiguration collection = null;
+    /** A reference to ourself so any inner-classes can dispose of us. */
+    private WriteCDImagePrompt prompt = null;
+    /** The close button, which exits the prompt without deleting anything. */
+    private JButton cancel_button = null;
+    /** The ok button which causes the selected collection to be deleted. */
+    private JButton ok_button = null;
+    /** The label above details. */
+    private JLabel details_label = null;
+    /** The label above the list. */
+    private JLabel list_label = null;
+    /** The text area used to display details about the collection selected. */
+    private JTextArea details_textarea = null;
+    /** The text area used to display instructions for the cd-rom/dvd export */
+    private JTextArea instructions_textarea;
+
+    /** The radio button for switching on the noinstall option */
+    private JRadioButton noinstall_button = null;
+    private JRadioButton install_button = null;
+    private JTextField title_field = null;
+    private JTextField estimated_size_field = null;
+    private JLabel title_label = null;
+    private JLabel estimated_size_label = null;
+    /** A string array used to pass arguments to the exportcol.pl script */
+    private String args[] = null;
+    private String cd_title = null;
+    /** whether the exporting was successful or not */
+    private boolean successful = false;
+    /** whether we are trying to export or not */
+    private boolean exporting = false;
+    /** the error message if any */
+    private StringBuffer error_message = null;
+    /** This is the size of the exported stuff on its own without collections. */
+    private long total_exported_size = 14000000;
+    /** The size of the export prompt screen. */
+    public static final Dimension SIZE = new Dimension(500, 500);
+
+    /** Constructor.
+     * @see org.greenstone.gatherer.collection.WriteCDImagePrompt.CancelButtonListener
+     * @see org.greenstone.gatherer.collection.WriteCDImagePrompt.CollectionListListener
+     * @see org.greenstone.gatherer.collection.WriteCDImagePrompt.OKButtonListener
+     */
+    public WriteCDImagePrompt() {
+	super(Gatherer.g_man, true);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	cancel_button = new GLIButton(Dictionary.get("General.Close"), Dictionary.get("General.Close_Tooltip"));
+	
+	details_textarea = new JTextArea(Dictionary.get("DeleteCollectionPrompt.No_Collection"));
+	details_textarea.setComponentOrientation(Dictionary.getOrientation());
+        details_textarea.setEditable(false);
+	
+	details_label = new JLabel(Dictionary.get("DeleteCollectionPrompt.Collection_Details"));
+	details_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	instructions_textarea = new JTextArea(Dictionary.get("WriteCDImagePrompt.Instructions"));
+        instructions_textarea.setComponentOrientation(Dictionary.getOrientation());
+	instructions_textarea.setCaretPosition(0);
+	instructions_textarea.setEditable(false);
+	instructions_textarea.setLineWrap(true);
+	instructions_textarea.setRows(4);
+	instructions_textarea.setWrapStyleWord(true);
+	
+	all_collections = new ArrayList();
+	list = new CheckList(true);
+	list_label = new JLabel(Dictionary.get("DeleteCollectionPrompt.Collection_List"));
+	list_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	ok_button = new GLIButton(Dictionary.get("WriteCDImagePrompt.Export"), Dictionary.get("WriteCDImagePrompt.Export_Tooltip"));	       
+        
+	title_field = new JTextField();
+        title_field.setComponentOrientation(Dictionary.getOrientation());
+	// Dictionary.setTooltip(title_field, "WriteCDImagePrompt.CD_Name_Tooltip");
+	title_label = new JLabel(Dictionary.get("WriteCDImagePrompt.CD_Name"));
+	title_label.setComponentOrientation(Dictionary.getOrientation());
+
+	estimated_size_field = new JTextField();
+        estimated_size_field.setComponentOrientation(Dictionary.getOrientation());
+	estimated_size_field.setEditable(false);
+
+	// work out the size of the images directory - could be large if they
+	// have the classic interface pack installed
+	total_exported_size += getFileSize(new File(LocalGreenstone.getDirectoryPath()+File.separator+"images"));
+	estimated_size_field.setText(Utility.formatFileLength(total_exported_size));
+	estimated_size_field.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	estimated_size_label = new JLabel(Dictionary.get("WriteCDImagePrompt.Size_Label"));
+	estimated_size_label.setComponentOrientation(Dictionary.getOrientation());
+        
+	scanForCollections();
+	list.setListData(all_collections);        
+
+	prompt = this;
+	setSize(SIZE);
+	setTitle(Dictionary.get("WriteCDImagePrompt.Export"));
+	
+	setJMenuBar(new SimpleMenuBar("exportingcollections")); 
+	cancel_button.addActionListener(new CancelButtonListener());
+	list.addListSelectionListener(new CollectionListListener());
+	list.clearSelection();
+	list.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+	ok_button_listener = new OKButtonListener();
+	ok_button.addActionListener(ok_button_listener);
+	ok_button.setEnabled(false);
+	//title.getDocument().addDocumentListener(new DocumentListener());
+	
+	noinstall_button = new JRadioButton(Dictionary.get("WriteCDImagePrompt.NoInstall"));
+	noinstall_button.setToolTipText(Dictionary.get("WriteCDImagePrompt.NoInstall_Tooltip"));
+	noinstall_button.setComponentOrientation(Dictionary.getOrientation());
+	
+	install_button = new JRadioButton(Dictionary.get("WriteCDImagePrompt.Install"));
+	install_button.setToolTipText(Dictionary.get("WriteCDImagePrompt.Install_Tooltip"));
+	install_button.setComponentOrientation(Dictionary.getOrientation());
+        
+    }
+
+    /** Destructor. */
+    public void destroy() {
+	all_collections.clear();
+	all_collections = null;
+	cancel_button = null;
+	details_textarea = null;
+	details_label = null;
+	list = null;
+	ok_button = null;
+	prompt = null;
+	if (selected_collections!=null) {
+	    selected_collections.clear();
+	    selected_collections = null;
+	}
+	title_field = null;
+	title_label = null;
+    }
+
+    /** This method causes the modal prompt to be displayed. 
+     * returns true if it has exported the collections that are currently selected */
+    public boolean display() {
+
+        JScrollPane scrol_tmp;
+	// Radio buttons
+	ButtonGroup radio_group = new ButtonGroup();        
+	radio_group.add(install_button);
+	install_button.setSelected(true);
+	radio_group.add(noinstall_button);
+
+	// Top pane
+	JPanel instructions_pane = new JPanel(new BorderLayout());
+        instructions_pane.setComponentOrientation(Dictionary.getOrientation());
+	instructions_pane.setBorder(BorderFactory.createEmptyBorder(5,5,0,5));
+        scrol_tmp = new JScrollPane(instructions_textarea);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	instructions_pane.add(scrol_tmp, BorderLayout.CENTER);
+	
+	title_label.setBorder(BorderFactory.createEmptyBorder(0,5,0,15));
+
+	JPanel title_pane = new JPanel(new BorderLayout());
+        title_pane.setComponentOrientation(Dictionary.getOrientation());
+	title_pane.add(title_label, BorderLayout.LINE_START);
+	title_pane.add(title_field, BorderLayout.CENTER);
+	title_pane.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));
+
+	JPanel radio_pane = new JPanel(new GridLayout(2,1));
+        radio_pane.setComponentOrientation(Dictionary.getOrientation());
+	install_button.setBackground(Configuration.getColor("coloring.collection_heading_background", true));
+	noinstall_button.setBackground(Configuration.getColor("coloring.collection_heading_background", true));
+	radio_pane.add(install_button);
+	radio_pane.add(noinstall_button);		
+        
+	JPanel options_pane = new JPanel(new BorderLayout());
+        options_pane.setComponentOrientation(Dictionary.getOrientation());
+	options_pane.add(title_pane, BorderLayout.NORTH);
+	options_pane.add(radio_pane, BorderLayout.CENTER);
+	instructions_pane.add(options_pane, BorderLayout.SOUTH);
+
+	// Central pane
+	JPanel list_pane = new JPanel(new BorderLayout());
+        list_pane.setComponentOrientation(Dictionary.getOrientation());
+	list_pane.add(list_label, BorderLayout.NORTH);
+        scrol_tmp =new JScrollPane(list);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	list_pane.add(scrol_tmp, BorderLayout.CENTER);
+	list_pane.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
+
+	JPanel details_pane = new JPanel(new BorderLayout());
+	details_pane.add(details_label, BorderLayout.NORTH);
+        scrol_tmp =new JScrollPane(details_textarea);
+        scrol_tmp.setComponentOrientation(Dictionary.getOrientation());
+	details_pane.add(scrol_tmp, BorderLayout.CENTER);
+	details_pane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
+        
+	JPanel central_pane = new JPanel(new GridLayout(2, 1));
+        central_pane.setComponentOrientation(Dictionary.getOrientation());
+	central_pane.add(list_pane);
+	central_pane.add(details_pane);
+	central_pane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+
+	// Lower pane
+	JPanel estimated_size_pane = new JPanel(new BorderLayout());
+        estimated_size_pane.setComponentOrientation(Dictionary.getOrientation());
+	estimated_size_pane.add(estimated_size_label, BorderLayout.LINE_START);
+	estimated_size_pane.add(estimated_size_field, BorderLayout.CENTER);
+	estimated_size_pane.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
+
+	JPanel button_pane = new JPanel(new GridLayout(1, 2));
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	button_pane.add(ok_button);
+	button_pane.add(cancel_button);
+	button_pane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
+       
+	JPanel lower_pane = new JPanel(new BorderLayout());
+        lower_pane.setComponentOrientation(Dictionary.getOrientation());
+	lower_pane.add(estimated_size_pane, BorderLayout.NORTH);
+	lower_pane.add(button_pane, BorderLayout.SOUTH);
+	lower_pane.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5));
+
+	// Final.
+	JPanel content_pane = (JPanel)this.getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(instructions_pane, BorderLayout.NORTH);
+	content_pane.add(central_pane, BorderLayout.CENTER);
+	content_pane.add(lower_pane, BorderLayout.SOUTH);
+        
+	// Center and display.
+	Dimension screen_size = Configuration.screen_size;
+	this.setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	this.setVisible(true); // blocks until the dialog is killed
+	return true;
+	
+    }
+
+    
+    private long getFileSize(File file)
+    {
+	long file_size = 0;
+	
+	// Directory case
+	if (file.isDirectory()) {
+	    File files[] = file.listFiles();
+	    for (int i = 0; i < files.length; i++) {
+		file_size += getFileSize(files[i]);
+	    }
+	}
+	// File case
+	else {
+	    file_size = file.length();
+	}
+	
+	return file_size;
+    }
+
+    /** This method calls the builcol.pl scripts via a GShell so as to not lock up the processor.
+     * @see org.greenstone.gatherer.Configuration
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.collection.Collection
+     * @see org.greenstone.gatherer.gui.BuildOptions
+     * @see org.greenstone.gatherer.shell.GShell
+     * @see org.greenstone.gatherer.shell.GShellListener
+     * @see org.greenstone.gatherer.shell.GShellProgressMonitor
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    public void writeCDImageCollections()
+    {
+	DebugStream.println("WriteCDImagePrompt.writeCDImageCollections()");
+
+	int num_collections = selected_collections.size();
+	if (num_collections == 0) return;
+	cd_title = title_field.getText();
+	cd_title = cd_title.trim();
+	cd_title = cd_title.replaceAll("\"","");
+	
+	// Generate the exportcol.pl command
+	ArrayList command_parts_list = new ArrayList();
+	if (!Gatherer.isGsdlRemote) {
+	    command_parts_list.add(Configuration.perl_path);
+	    command_parts_list.add("-S");
+	}
+	command_parts_list.add(LocalGreenstone.getBinScriptDirectoryPath() + "exportcol.pl");
+
+	command_parts_list.add("-gli");
+	command_parts_list.add("-language");
+	command_parts_list.add(Configuration.getLanguage());
+
+	if (cd_title.equals("")) {
+	    command_parts_list.add("-cdname");
+	    command_parts_list.add("Greenstone Collections");
+
+	    command_parts_list.add("-cddir");
+	    command_parts_list.add("exported_collections");
+	}
+	else {
+	    command_parts_list.add("-cdname");
+	    command_parts_list.add(cd_title);
+
+	    String cd_dir = "exported_"+cd_title.replaceAll("\\s","");
+	    command_parts_list.add("-cddir");
+	    command_parts_list.add(cd_dir);
+	}
+
+	if (noinstall_button.isSelected()) {
+	    command_parts_list.add("-noinstall");
+	}
+
+	// we want to be able to export collections in other collect directories too
+	String collectDir = Gatherer.getCollectDirectoryPath();
+	if(collectDir != Gatherer.getDefaultGSCollectDirectoryPath(true)) {
+		command_parts_list.add("-collectdir");
+		command_parts_list.add(collectDir);
+	}
+	
+	for (int i = 0; i < num_collections; i++) {
+	    command_parts_list.add(((BasicCollectionConfiguration) selected_collections.get(i)).getShortName());
+	}
+
+	DebugStream.print("export command = ");
+	for (int i = 0; i < command_parts_list.size(); i++) {
+	    DebugStream.print(command_parts_list.get(i) + " ");
+	}
+	DebugStream.println("");
+	// Run the exportcol.pl command
+	String[] command_parts = (String[]) command_parts_list.toArray(new String[0]);
+	GShell process = new GShell(command_parts, GShell.CDIMAGE, 3, this, null, GShell.GSHELL_CDIMAGE);
+	process.start();
+	//process.run();
+	DebugStream.println("WriteCDImagePrompt.writeCDImageCollections().return");
+	
+    }
+
+    /** Shows an export complete prompt. 
+     * @param success A <strong>boolean</strong> indicating if the collection was successfully deleted.
+     * @see org.greenstone.gatherer.collection.Collection
+     */
+    public void resultPrompt(boolean success, String extra) {
+	args = new String[2];
+	StringBuffer coll_names = new StringBuffer();
+	for (int i=0; i<selected_collections.size();i++) {
+	    if (i>0) {
+		coll_names.append(", ");
+	    }
+	    BasicCollectionConfiguration complete_collection = (BasicCollectionConfiguration)selected_collections.get(i);
+	    coll_names.append(complete_collection.getName() + StaticStrings.SPACE_CHARACTER + StaticStrings.OPEN_PARENTHESIS_CHARACTER + complete_collection.getShortName() + StaticStrings.CLOSE_PARENTHESIS_CHARACTER);
+	    complete_collection = null;
+	}
+	
+	args[0] = coll_names.toString();
+	args[1] = LocalGreenstone.getTmpDirectoryPath();
+	if(cd_title.equals("")) {
+	    args[1] += "exported_collections";
+	} else {
+	    args[1] += "exported_"+cd_title.replaceAll("\\s","");
+	}
+	String title;
+	String label;
+	if (success) {
+	    title = Dictionary.get("WriteCDImagePrompt.Successful_Title");
+	    label = Dictionary.get("WriteCDImagePrompt.Successful_Export", args);
+	} else {
+	    title = Dictionary.get("WriteCDImagePrompt.Failed_Title");
+	    label = Dictionary.get("WriteCDImagePrompt.Failed_Export", args);
+	}
+	SimpleResultDialog result_dialog = new SimpleResultDialog(title, label, extra);
+	result_dialog.setVisible(true); // Blocks
+	result_dialog.dispose();
+	result_dialog = null;
+    }
+
+    /** Method to scan the collect directory retrieving and reloading each collection it finds, while building the list of known collections.
+     * @see org.greenstone.gatherer.Configuration
+     * @see org.greenstone.gatherer.Gatherer
+     * @see org.greenstone.gatherer.util.ArrayTools
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    private void scanForCollections() {
+	// Start at the collect dir.
+	File collect_directory = new File(Gatherer.getCollectDirectoryPath());
+	if (collect_directory.exists()) {
+	  scanForCollectionsInternal(collect_directory);
+	}
+    }
+
+  private void scanForCollectionsInternal(File collect_directory) {
+    
+    String file_name = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
+    // For each child directory see if it contains a .cfg file and
+    // if so try to load it..
+    File collections[] = collect_directory.listFiles();
+    ArrayTools.sort(collections);
+    for(int i = 0; collections != null && i < collections.length; i++) {
+      if(collections[i].isDirectory() && !collections[i].getName().equals(StaticStrings.MODEL_COLLECTION_NAME)) {
+	File config_file = new File(collections[i], file_name);
+	if (config_file.exists()) {
+	  BasicCollectionConfiguration config = new BasicCollectionConfiguration(config_file);
+	  if (config.getCollectGroup().equals("true")) {
+	    scanForCollectionsInternal(collections[i]);
+	  } else {
+	    all_collections.add(config);		        
+	    config = null;
+	  }
+	}
+      }
+    }
+  }
+
+
+    /** All implementation of GShellListener must include this method so the listener can be informed of messages from the GShell.
+     * @param event A <strong>GShellEvent</strong> that contains, amoung other things, the message.
+     */
+    public synchronized void message(GShellEvent event) {
+	// Ignore the messages from RecPlug with 'show_progress' set (used for progress bars)
+	String message = event.getMessage();
+	if (message.startsWith("exportcol.pl>")) {
+	    message = message.substring(13);
+	    //DebugStream.println("message = "+event.getMessage());
+	    error_message.append(message);
+	    error_message.append("\n");
+	}
+    }
+
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell begins its task. Implementation side-effect, not actually used.
+     * @param event A <strong>GShellEvent</strong> that contains details of the initial state of the <strong>GShell</strong> before task comencement.
+     */
+    public synchronized void processBegun(GShellEvent event) {
+	// We don't care. 
+    }
+    /** All implementation of GShellListener must include this method so the listener can be informed when a GShell completes its task.
+     * @param event A <strong>GShellEvent</strong> that contains details of the final state of the <strong>GShell</strong> after task completion.
+     */
+    public synchronized void processComplete(GShellEvent event) {
+	successful = false;
+	if(event.getStatus() == GShell.OK) {
+	    if(event.getType() == GShell.CDIMAGE) {
+		successful = true;
+	    }
+	}
+	ok_button_listener.processComplete();
+    }
+
+    /** A button listener implementation, which listens for actions on the close button and disposes of the dialog when detected. */
+    private class CancelButtonListener 
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so we can be informed when the button is actioned.
+	 * @param event An <strong>ActionEvent</strong> containing all the relevant information garnered from the event itself.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    prompt.dispose();
+	}
+    }
+
+    /** This private class listens for selection events in from the list and then displays the appropriate details for that collection.
+     */
+    private class CollectionListListener 
+	implements ListSelectionListener
+    {
+	/** Any implementation of ListSelectionListener must include this method so we can be informed when the list selection changes.
+	 * @param  event a <strong>ListSelectionEvent</strong> containing all the relevant information garnered from the event itself
+	 */
+	public void valueChanged(ListSelectionEvent event)
+	{
+	    // Wait for things to settle down a bit
+	    if (event.getValueIsAdjusting()) {
+		return;
+	    }
+
+	    // Can only export when something is ticked
+	    ok_button.setEnabled(!list.isNothingTicked());
+
+	    if (list.isSelectionEmpty()) {
+		// This only happens when the dialog is first entered
+		details_textarea.setText(Dictionary.get("DeleteCollectionPrompt.No_Collection"));
+		return;
+	    }
+
+	    collection = (BasicCollectionConfiguration) ((CheckListEntry) list.getSelectedValue()).getObject();
+	    args = new String[3];
+	    args[0] = collection.getCreator();
+	    args[1] = collection.getMaintainer();
+	    args[2] = collection.getDescription();
+	    details_textarea.setText(Dictionary.get("DeleteCollectionPrompt.Details", args));
+	    details_textarea.setCaretPosition(0);
+
+	    // Find the size of the "etc", "images" and "index" directories of the collection
+	    String collection_directory_path = CollectionManager.getCollectionDirectoryPath(collection.getShortName()); // (colgroup/)subcol
+	    File etc_directory = new File(collection_directory_path + "etc");
+	    File images_directory = new File(collection_directory_path + "images");
+	    File index_directory = new File(collection_directory_path + "index");
+	    long collection_size_built = getFileSize(etc_directory) + getFileSize(images_directory) + getFileSize(index_directory);
+
+	    // Add/subtract it from the total, depending on whether the collection has just been ticked/unticked
+	    if (((CheckListEntry) list.getSelectedValue()).isSelected()) {
+		total_exported_size += collection_size_built;
+	    }
+	    else {
+		total_exported_size -= collection_size_built;
+	    }
+
+	    // Update the size field
+	    estimated_size_field.setText(Utility.formatFileLength(total_exported_size));
+	}
+    }
+
+
+    /** The OK button listener implementation. */
+    private class OKButtonListener 
+	implements ActionListener {
+	private Component glass_pane;
+	private MouseListener mouse_blocker_listener;
+	private ProgressDialog progress_dialog;
+
+	/** Any implementation of ActionListener must include this method so we can be informed when the button is actioned.
+	 * @param event An <strong>ActionEvent</strong> containing all the relevant information garnered from the event itself.
+	 * @see org.greenstone.gatherer.Configuration
+	 * @see org.greenstone.gatherer.Gatherer
+	 * @see org.greenstone.gatherer.util.Utility
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    ///ystem.err.println("OK Clicked");
+	    // Make sure there are some colls specified
+	    selected_collections = list.getTicked();
+	    error_message = new StringBuffer();
+	    
+	    // Set the cursor to hourglass
+	    glass_pane = getGlassPane();
+	    mouse_blocker_listener = new MouseAdapter() {};
+	    glass_pane.addMouseListener(mouse_blocker_listener);
+	    glass_pane.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+	    glass_pane.setVisible(true);
+
+	    // Export the selected collection.
+	    ///ystem.err.println("Write CD-ROM/DVD image for named collections");
+	    writeCDImageCollections();
+
+	    // Show progress dialog
+	    ///ystem.err.println("Showing progress dialog");
+	    progress_dialog = new ProgressDialog();
+	    progress_dialog.setVisible(true);
+	}
+
+	public void processComplete() {
+	    ///ystem.err.println("Process complete");
+	    // Dispose of progress dialog
+	    progress_dialog.setVisible(false);
+	    progress_dialog.dispose();
+	    progress_dialog = null;
+
+	    // unset the cursor
+	    glass_pane.setVisible(false);
+	    glass_pane.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+	    glass_pane.removeMouseListener(mouse_blocker_listener);
+	    glass_pane = null;
+	    mouse_blocker_listener= null;
+	    
+	    if (successful) {
+		resultPrompt(true, error_message.toString());
+	    } else {
+		resultPrompt(false, error_message.toString());
+	    }
+	    error_message = null;
+	}
+
+	private class ProgressDialog 
+	    extends ModalDialog {
+	    
+	    private Dimension size = new Dimension(400,65);
+	    
+	    public ProgressDialog() {
+		super(Gatherer.g_man, Dictionary.get("WriteCDImagePrompt.Title"), true);
+                this.setComponentOrientation(Dictionary.getOrientation());
+		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+		setSize(size);
+		JPanel content_pane = (JPanel) getContentPane();
+                content_pane.setComponentOrientation(Dictionary.getOrientation());
+		JLabel progress_label = new JLabel(Dictionary.get("WriteCDImagePrompt.Progress_Label"));
+		progress_label.setComponentOrientation(Dictionary.getOrientation());
+                
+		JProgressBar progress_bar = new JProgressBar();
+                progress_bar.setComponentOrientation(Dictionary.getOrientation());
+		progress_bar.setIndeterminate(true);
+		content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+		content_pane.setLayout(new BorderLayout());
+		content_pane.add(progress_label, BorderLayout.NORTH);
+		content_pane.add(progress_bar, BorderLayout.CENTER);
+		// Position
+		Rectangle frame_bounds = Gatherer.g_man.getBounds();
+		setLocation(frame_bounds.x + (frame_bounds.width - size.width) / 2, frame_bounds.y + (frame_bounds.height - size.height) / 2);
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/Autofilter.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/Autofilter.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/Autofilter.java	(revision 31635)
@@ -0,0 +1,379 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui.metaaudit;
+
+
+import java.util.ArrayList;
+import org.greenstone.gatherer.util.StaticStrings;
+
+/** An Autofilter object stores the filters set on a single column, and provides a method for determining if a certain value passes the filter.
+ * @author John Thompson
+ * @version 2.3
+ */
+public class Autofilter {
+
+    static public void main(String[] args) {
+	if(args.length != 2) {
+	    System.err.println("Usage: java Autofilter <string> <pattern>");
+	}
+	else {
+	    switch(compareToPattern(args[0], args[1])) {
+	    case LESS_THAN:
+		System.err.print(args[0] + " is less than");
+		break;
+	    case GREATER_THAN:
+		System.err.print(args[0] + " is greater than");
+		break;
+	    case EQUAL:
+		System.err.print(args[0] + " is equal to");
+		break;
+	    default:
+		System.err.print(args[0] + " is something to");
+	    }
+	    System.err.println(" " + args[1]);
+	}
+    }
+
+    /** A String comparison which also recognises the WildCard character. The basic idea is that '*' matches zero or more of any characters, and if we limited patterns to be like "a*" then as soon as we hit the star we could return a match. The problem is that we want patterns like "b*y" which would match "boy" but not "bee" nor "bus" (but should return less than or greater than for each of these respectively). Thus our testing has to become non-deterministic so the matches are conducted like so: 
+     * Given p = [ b * y ]
+     * Test  s = [ b e e ]  Result: p0=s0,{(p2>s1=>ERROR),(p1=s1,{(p1>s2=>ERROR),(p2>s2=>LESS_THAN)})} => LESS_THAN
+     * Test  s = [ b o y ]  Result: p0=s0,{(p2>s1=>ERROR),(p1=s1,{(p1>s2=>ERROR),(p2=s2=>EQUAL)})} => EQUAL
+     * Test  s = [ b u s ]  Result: p0=s0,{(p2>s1=>ERROR),(p1=s1,{(p1>s2=>ERROR),(p2<s2=>GREATER_THAN)})} => GREATER_THAN
+     * where the two () phrases within a {} represent two different 'threads' of processing. Errors are generated when one string runs out of characters before the other. Only non-error results are propogated. I've decided to implement this non-deterministic matching as a recursive function call.
+     * If you analyse it you can see there are three possible actions when a '*' is detected.
+     * 1. * is an empty string, ignore it in pattern and try to compare the next character in p with the current character in s. This case only occurs if there are more characters in p (otherwise apple would be greater than a*)
+     * 2. * matches exactly the current character in s, try to match the next character in p with the next character in s
+     * 3. * matches some string of characters including the current character in s, try to match '*' against the next character in s
+     * @param  s the String being matched
+     * @param  p the Pattern to match it against
+     * @return -1 if s is less than p, 0 if they are equal, 1 if s is greater than p
+     */
+    static private int compareToPattern(String s, String p) {
+	if(p.indexOf(StaticStrings.STAR_CHAR) == -1) {
+	    return compareToPattern(s, p, 0, 0, false);
+	}
+	else {
+	    return compareToPattern(s, p, 0, 0, true);
+	}
+    }
+
+    static final private int LESS_THAN = -1;
+    static final private int EQUAL = 0;
+    static final private int GREATER_THAN = 1;
+    static final private int ERROR = 42;
+
+    static private int compareToPattern(String s, String p, int s_index, int p_index, boolean wildcard_matching) {
+	// Lets do the simple cases first.
+	// Both out of characters at the same time
+	if(s_index >= s.length() && p_index >= p.length()) {
+	    ///ystem.err.println("Both out of characters. Equal.");
+	    return EQUAL;
+	}
+	// s out of characters first
+	if(s_index >= s.length()) {
+	    // Not allowed to run out if matching wildcard
+	    if(wildcard_matching) {
+		return ERROR;
+	    }
+	    // Remember that wildcard matches the empty string too, so as long as there are only '*' characters left in the pattern we can say they are equal as well.
+	    else {
+		while(p_index < p.length()) {
+		    if(p.charAt(p_index) == StaticStrings.STAR_CHAR) {
+			p_index++;
+		    }
+		    else {
+			///ystem.err.println("S out of characters. Less Than.");
+			return LESS_THAN;
+		    }
+		}
+		// We've run out of pattern and it only contained '*' so we're still equal
+		///ystem.err.println("Both out of characters. Equal.");
+		return EQUAL;
+	    }
+	}
+	// p out of characters first
+	if(p_index >= p.length()) {
+	    // Not allowed to run out if matching wildcard
+	    if(wildcard_matching) {
+		return ERROR;
+	    }
+	    // so it is greater than
+	    else {
+		///ystem.err.println("P out of characters. Greater Than.");
+		return GREATER_THAN;
+	    }
+	}
+	char s_char = s.charAt(s_index);
+	char p_char = p.charAt(p_index);
+	///ystem.err.println("Comparing " + s_char + " against pattern character " + p_char);
+	// Equivelent characters
+	// Now the tricky-dicky bit - WildCard matching.
+	if(p_char == StaticStrings.STAR_CHAR) {
+	    int result;
+	    // Case 1
+	    ///ystem.err.println("WildCard Case 1");
+	    if(p_index + 1 < p.length()) {
+		if((result = compareToPattern(s, p, s_index, p_index + 1, wildcard_matching)) != ERROR) {
+		    return result;
+		}
+	    }
+	    // Case 2
+	    ///ystem.err.println("WildCard Case 2");
+	    if((result = compareToPattern(s, p, s_index + 1, p_index + 1, wildcard_matching)) != ERROR) {
+		return result;
+	    } 
+	    // Case 3
+	    ///ystem.err.println("WildCard Case 3");
+	    if((result = compareToPattern(s, p, s_index + 1, p_index, wildcard_matching)) != ERROR) {
+		return result;
+	    }
+	}
+	if(s_char == p_char) {
+	    return compareToPattern(s, p, s_index + 1, p_index + 1, wildcard_matching);
+	}
+	// A preceeding character
+	if(s_char < p_char) {
+	    ///ystem.err.println("s char is less than p char");
+	    if(wildcard_matching) {
+		///ystem.err.println("Because we are wildcard matching we still have to match the rest of s incase of errors.");
+		if(compareToPattern(s, p, s_index + 1, p_index + 1, wildcard_matching) != ERROR) {
+		    ///ystem.err.println("No error. Less than.");
+		    return LESS_THAN;
+		}
+		else {
+		    ///ystem.err.println("Error detected.");
+		    return ERROR;
+		}
+	    }
+	    else {
+		///ystem.err.println("Less than.");
+		return LESS_THAN;
+	    }
+	}
+	// A succeeding character
+	if(s_char > p_char) {
+	    ///ystem.err.println("s char is greater than p char");
+	    if(wildcard_matching) {
+		///ystem.err.println("Because we are wildcard matching we still have to match the rest of s incase of errors.");
+		if(compareToPattern(s, p, s_index + 1, p_index + 1, wildcard_matching) != ERROR) {
+		    ///ystem.err.println("No error. Greater than.");
+		    return GREATER_THAN;
+		}
+		else {
+		    ///ystem.err.println("Error detected.");
+		    return ERROR;
+		}
+	    }
+	    else {
+		///ystem.err.println("Greater than.");
+		return GREATER_THAN;
+	    }
+	}
+	// Oh-No. Well, we'll just assume that string is less than pattern
+	return LESS_THAN;
+    }
+
+    /** <i>true</i> if the filter should be applied, <i>false</i> to indicate the filter is turned off. */
+    public boolean active;
+    /** <i>true</i> if the matching for the first expression should be case sensitive, <i>false</i> otherwise. */
+    public boolean casesense_one;
+    /** <i>true</i> if the matching for the second expression should be case sensitive, <i>false</i> otherwise. */
+    public boolean casesense_two;
+    /** Used to determine the operation intended when applying two filters, and set using the values of the OPERATION_TYPE enumeration. */
+    public boolean operation;
+    /** Used to determine how the column this filter is applied to should be sorted, and set using the values of the SORT_TYPE enumeration. */
+    public boolean sort;
+    /** The method to be used for the first filter expression, set from the values of the METHOD_LIST enumeration. */
+    public int method_one;
+    /** The method to be used for the second filter expression, set from the values of the METHOD_LIST enumeration. */
+    public int method_two;
+    /** The value to be matched against for the first expression. */
+    public String value_one;
+    /** The value to be matched against for the second expression. */
+    public String value_two;
+    /** An element of the SORT_TYPE enumeration, indicates lowest to highest value column ordering. */
+    public static final boolean ASCENDING = true;
+    /** An element of the OPERATION_TYPE enumeration, indicates that both filter expressions must be met (conjunction). */
+    public static final boolean AND = true;
+    /** An element of the SORT_TYPE enumeration, indicates highest to lowest value column ordering. */
+    public static final boolean DESCENDING = false;
+    /** An element of the OPERATION_TYPE enumeration, indicates that either (or both) filter expressions must be met (disjunction). */
+    public static final boolean OR = false;
+    /** An enumeration of symbolic names of various matching methods. */
+    public static final String METHOD_LIST[] = { "Autofilter.eqeq", "Autofilter.!eq", "Autofilter.<", "Autofilter.<eq", "Autofilter.>", "Autofilter.>eq", "Autofilter.^", "Autofilter.!^", "Autofilter.$", "Autofilter.!$", "Autofilter.?", "Autofilter.!?" };
+
+    /** Default Constructor. */
+    public Autofilter() {
+	operation = OR;
+	sort = ASCENDING;
+    }
+    /** Determine if this filter is currently active.
+     * @return <i>true</i> if it is active, <i>false</i> otherwise.
+     */
+    public boolean active() {
+	return active;
+    }
+    /** Determine if this list of values (for a certain cell) passes the filter.
+     * @param values An <strong>ArrayList</strong> of values sourced from a single cell in the associated column.
+     * @return <i>true</i> if the values match and should be displayed, <i>false</i> otherwise.
+     */
+    public boolean filter(ArrayList values) {
+	boolean result = false;
+	if(value_one != null) {
+	    result = filter(values, method_one, value_one, casesense_one);
+	    if(result) {
+		if(operation == AND && value_two != null) {
+		    result = filter(values, method_two, value_two, casesense_two);
+		}
+	    }
+	    else if(operation == OR && value_two != null) {
+		result = filter(values, method_two, value_two, casesense_two);
+	    }
+	}
+	return result;
+    }
+    /** Set the current activity state of this filter.
+     * @param active The new state of this filter, <i>true</i> to activate, <i>false</i> otherwise.
+     */
+    public void setActive(boolean active) {
+	this.active = active;
+    }
+    /** Set one of the filter expressions using the given information.
+     * @param number The number of the filter you wish to set as an <i>int</i>. Either 1 or 2.
+     * @param method An <i>int</i> indicating the method to be used when matching.
+     * @param value The <strong>String</strong> to be matched against.
+     * @param casesense <i>true</i> if this expression should be case sensitive, <i>false</i> otherwise.
+     */
+    public void setFilter(int number, int method, String value, boolean casesense) {
+	if(!casesense && value != null) {
+	    value = value.toLowerCase();
+	}
+	if(number == 1) {
+	    casesense_one = casesense;
+	    method_one = method;
+	    value_one = value;
+	}
+	else {
+	    casesense_two = casesense;
+	    method_two = method;
+	    value_two = value;
+	}
+    }
+    /** Set the operation to be used to join the two filters (if a second filter is set).
+     * @param operation <i>true</i> for conjunct filters, <i>false</i> for disjunct.
+     */ 
+    public void setOperation(boolean operation) {
+	this.operation = operation;
+    }
+    /** Set the sort order of this column.
+     * @param sort <i>true</i> for ascending sort, <i>false</i> for descending.
+     */
+    public void setSort(boolean sort) {
+	this.sort = sort;
+    }
+    /** Decide whether a row should be displayed or filtered. The result depends on the selector set with setFilterType.
+     * @param values An <strong>ArrayList</strong> of values to be checked against the filter.
+     * @param method The method of matching to be used, as an <i>int</i>.
+     * @param target The <strong>String</Strong> to match against.
+     * @param casesense <i>true</i> if the match is to be case sensitive, <i>false</i> otherwise.
+     * @return <i>true</i> to display the row, <i>false</i> to hide it.
+     */
+    public boolean filter(ArrayList values, int method, String target, boolean casesense) {
+	boolean result = false;
+	// There are several special cases when the filter always returns turn, such as when the target is the wildcard character.
+	if(target == null || target.length() == 0 || target.equals("*")) {
+	    result = true;
+	}
+	else {
+	    // For each value in the list...
+	    for(int i = 0; i < values.size(); i++) {
+		boolean pass;
+		String source;
+		// Account for case sensitivity.
+		if(casesense) {
+		    source = values.get(i).toString();
+		}
+		else {
+		    source = values.get(i).toString().toLowerCase();
+		}
+		///ystem.err.println("Testing " + source + " against pattern " + target);
+		// Perform the match, based on the selected method.
+		switch(method) {
+		case 1: // !EQ
+		    pass = (compareToPattern(source, target) != 0);
+		    break;
+		case 2: // <
+		    pass = (compareToPattern(source, target) < 0);
+		    break;
+		case 3: // <eq
+		    pass = (compareToPattern(source, target) <= 0);
+		    break;
+		case 4: // >
+		    pass = (compareToPattern(source, target) > 0);
+		    break;
+		case 5: // >eq
+		    pass = (compareToPattern(source, target) >= 0);
+		    break;
+		case 6: // ^
+		    pass = source.startsWith(target);
+		    break;
+		case 7: // !^
+		    pass = !(source.startsWith(target));
+		    break;
+		case 8: // $
+		    pass = source.endsWith(target);
+		    break;
+		case 9: // !$
+		    pass = !(source.endsWith(target));
+		    break;
+		case 10: // ?
+		    pass = (source.indexOf(target) != -1);
+		    break;
+		case 11: // !?
+		    pass = (source.indexOf(target) == -1);
+		    break;
+		default: // EQEQ
+		    pass = (compareToPattern(source, target) == 0);
+		    break;
+		}
+		result = result || pass;
+	    }
+	}
+	return result;		 
+    }
+    /** Produce a textual representation of this autofilter.
+     * @return A <strong>String</strong> displaying details of this autofilter.
+     */
+    public String toString() {
+	String result = "One: " + method_one + " - " + value_one + " - " + casesense_one;
+	if(value_two != null) {
+	    result = result + "\n" + "Operation: " + (operation?"AND":"OR") + "\n";
+	    result = result + "Two: " + method_two + " - " + value_two + " - " + casesense_two;
+	}
+	return result;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/AutofilterDialog.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/AutofilterDialog.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/AutofilterDialog.java	(revision 31635)
@@ -0,0 +1,440 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui.metaaudit;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.SimpleMenuBar;
+import org.greenstone.gatherer.gui.ModalDialog;
+
+/** The autofilter concept comes from Microsoft Excel spreadsheets that use autofilters to filter to the sheet. When you click on the heading of a column, a new prompt allows you to specify what filter should be acting apon the selected column. Any new filter is conjoined with any previous filters to provide a sheet containing only rows that match all current filters. Each column must also provide an indicator for determining if a filter is set (in this case a special icon) and and a method for removing a filter (use the clear filter button within the autofilter dialog). Having recently discovered that most JVMs aren't very good at recoving memory used by dialog and frame windows, special care must be made to deallocate all references properly, as a user may open several dozen autofilter prompts over the lifetime of a session.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public final class AutofilterDialog
+    extends ModalDialog {
+    /** The filter being edited. */
+    private Autofilter filter;
+    /** A reference to ourselves so our inner classes can reference us. */
+    private AutofilterDialog self;
+
+    private boolean cancelled;
+    /** The value returned from the filter dialog prompt. Used to determine if a prompt was set or unset, and what subsequent action to take. */
+    private byte return_value = 0;
+    /** The button used to cancel the prompt. */
+    private JButton cancel_button = null;
+    private JButton remove_button = null;
+    private JButton set_button = null;
+    /** Used to specify that the given filters should be applied conjunctly. */
+    private JRadioButton and_radiobutton = null;
+    /** The check box used to specify whether the first filter is case sensitive. */
+    private JCheckBox first_case = null;
+    /** Used to disable the second filter. */
+    private JRadioButton none_radiobutton = null;
+    /** Used to specify that the given filters should be applied disjunctly. */
+    private JRadioButton or_radiobutton = null;
+    /** The check box used to specify whether the second filter is case sensitive. */
+    private JCheckBox second_case = null;
+    /** Used to specify the order of the resulting set: Ascending or Descending. */
+    private JRadioButton ascending_radiobutton = null;
+    private JRadioButton descending_radiobutton = null;
+    /** The method used to match the first filter: Contains, Doesn't contain etc. */
+    private JComboBox first_method = null;
+    /** The value to be matched for the first filter. */
+    private JComboBox first_value = null;
+    /** The method used to match the first filter. Options as for the first method. */
+    private JComboBox second_method = null;
+    /** The value to be matched for the second filter. */
+    private JComboBox second_value = null;
+    /** Used for the most basic filter where an Equals, Case sensitive method is automatically used. */
+    private JComboBox value = null;
+    /** The label which displays the name of the currently selected column (the column that will be associated with the autofilter this dialog produces). */
+    private JLabel name;
+    /** The autofilter prompt contains two different panes, one for basic functionality and one for advanced. This containiner component is used to allow access to each via a 'tabbed' interface. */
+    private JTabbedPane control = null;
+    /** A reference back to the MetaAudit dialog that spawned this prompt. Used to make sure that any open dialog window is always in front of the audit pane. */
+    private MetaAuditFrame dialog;
+    /** The default size for the autofilter control. */
+    static final private Dimension SIZE = new Dimension(640, 245);
+
+    /** Constructor.
+     * @param dialog A reference to the <strong>MetaAuditFrame</strong> that spawned this dialog.
+     * @see org.greenstone.gatherer.gui.metaaudit.Autofilter
+     * @see org.greenstone.gatherer.gui.metaaudit.AutofilterDialog.ButtonListener
+     * @see org.greenstone.gatherer.gui.metaaudit.AutofilterDialog.CheckListener
+     */
+    public AutofilterDialog(MetaAuditFrame dialog) {
+	super(Gatherer.g_man);
+	this.dialog = dialog;
+	this.self = this;
+        this.setComponentOrientation(Dictionary.getOrientation());
+	setModal(true);
+	setJMenuBar(new SimpleMenuBar("reviewingmetadata")); 
+	setSize(SIZE);
+	setTitle(Dictionary.get("Autofilter.Title"));
+	
+	// Creation
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel name_pane = new JPanel();
+        name_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel name_label = new JLabel(Dictionary.get("Autofilter.Name"));
+        name_label.setComponentOrientation(Dictionary.getOrientation());
+	JTextField name_template = new JTextField();
+        name_template.setComponentOrientation(Dictionary.getOrientation());
+	name = new JLabel();
+        name.setComponentOrientation(Dictionary.getOrientation());
+	name.setBorder(name_template.getBorder());
+	control = new JTabbedPane();
+        control.setComponentOrientation(Dictionary.getOrientation());
+	JPanel value_pane = new JPanel();
+        value_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel inner_value_pane = new JPanel();
+        inner_value_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel value_label = new JLabel(Dictionary.get("Autofilter.eqeq"));
+        value_label.setComponentOrientation(Dictionary.getOrientation());
+	value = new JComboBox();
+        value.setComponentOrientation(Dictionary.getOrientation());
+	value.setOpaque(false);
+	value.setEditable(false);
+	JPanel custom_pane = new JPanel();
+        custom_pane.setComponentOrientation(Dictionary.getOrientation());
+
+	JPanel first_pane = new JPanel();
+        first_pane.setComponentOrientation(Dictionary.getOrientation());
+	first_method = new JComboBox();
+        first_method.setComponentOrientation(Dictionary.getOrientation());
+	first_method.setOpaque(false);
+	first_value = new JComboBox();
+        first_value.setComponentOrientation(Dictionary.getOrientation());
+	first_value.setOpaque(false);
+	first_value.setEditable(true);
+	first_value.addItem("");
+	first_value.setSelectedItem("");
+	first_case = new JCheckBox(Dictionary.get("Autofilter.Case_Sensitive"));
+        first_case.setComponentOrientation(Dictionary.getOrientation());
+	JPanel operator_pane = new JPanel();
+        operator_pane.setComponentOrientation(Dictionary.getOrientation());
+	JLabel operator_label = new JLabel(Dictionary.get("Autofilter.Operator"));
+        operator_label.setComponentOrientation(Dictionary.getOrientation());
+	ButtonGroup operator_group = new ButtonGroup();        
+	and_radiobutton = new JRadioButton(Dictionary.get("Autofilter.AND"));
+        and_radiobutton.setComponentOrientation(Dictionary.getOrientation());
+	and_radiobutton.setOpaque(false);
+	none_radiobutton = new JRadioButton(Dictionary.get("Autofilter.None"));
+        none_radiobutton.setComponentOrientation(Dictionary.getOrientation());
+	none_radiobutton.setOpaque(false);
+	none_radiobutton.setSelected(true);
+	or_radiobutton = new JRadioButton(Dictionary.get("Autofilter.OR"));
+        or_radiobutton.setComponentOrientation(Dictionary.getOrientation());
+	or_radiobutton.setOpaque(false);
+	operator_group.add(none_radiobutton);
+	operator_group.add(and_radiobutton);
+	operator_group.add(or_radiobutton);
+
+	JPanel second_pane = new JPanel();
+        second_pane.setComponentOrientation(Dictionary.getOrientation());
+	second_method = new JComboBox();
+        second_method.setComponentOrientation(Dictionary.getOrientation());
+	second_method.setOpaque(false);
+	second_method.setEnabled(false);
+	second_value = new JComboBox();
+        second_value.setComponentOrientation(Dictionary.getOrientation());
+	second_value.setOpaque(false);
+	second_value.setEditable(true);
+	second_value.setEnabled(false);
+	second_value.addItem("");
+	second_value.setSelectedItem("");
+	second_case = new JCheckBox(Dictionary.get("Autofilter.Case_Sensitive"));
+        second_case.setComponentOrientation(Dictionary.getOrientation());
+	JPanel lower_pane = new JPanel();
+        lower_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel order_pane = new JPanel();
+        order_pane.setComponentOrientation(Dictionary.getOrientation());
+	order_pane.setToolTipText(Dictionary.get("Autofilter.Order_Tooltip"));
+	JLabel order_label = new JLabel(Dictionary.get("Autofilter.Order"));
+	order_label.setComponentOrientation(Dictionary.getOrientation());
+	ButtonGroup order_group = new ButtonGroup();
+	ascending_radiobutton = new JRadioButton(Dictionary.get("Autofilter.Ascending"));
+        ascending_radiobutton.setComponentOrientation(Dictionary.getOrientation());
+	ascending_radiobutton.setOpaque(false);
+	ascending_radiobutton.setSelected(true);
+	descending_radiobutton = new JRadioButton(Dictionary.get("Autofilter.Descending"));
+        descending_radiobutton.setComponentOrientation(Dictionary.getOrientation());
+	descending_radiobutton.setOpaque(false);
+	order_group.add(ascending_radiobutton);
+	order_group.add(descending_radiobutton);
+
+	// Assign values to method comboboxes.
+	for(int i = 0; i < Autofilter.METHOD_LIST.length; i++) {
+	    first_method.addItem(Dictionary.get(Autofilter.METHOD_LIST[i]));
+	    second_method.addItem(Dictionary.get(Autofilter.METHOD_LIST[i]));
+	}
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+	cancel_button = new GLIButton(Dictionary.get("General.Cancel"), Dictionary.get("General.Pure_Cancel_Tooltip"));
+	remove_button = new GLIButton(Dictionary.get("Autofilter.Remove"), Dictionary.get("Autofilter.Remove_Tooltip"));
+	set_button = new GLIButton(Dictionary.get("Autofilter.Set"), Dictionary.get("Autofilter.Set_Tooltip"));
+	
+	// Connection
+	and_radiobutton.addActionListener(new CheckListener(true));
+	none_radiobutton.addActionListener(new CheckListener(false));
+	or_radiobutton.addActionListener(new CheckListener(true));
+	cancel_button.addActionListener(new ButtonListener()); // It returns any currently set filter
+	remove_button.addActionListener(new ButtonListener(false));
+	set_button.addActionListener(new ButtonListener(true));
+
+	// Layout		  
+	name_label.setBorder(BorderFactory.createEmptyBorder(0,0,0,5));
+
+	name_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+	name_pane.setLayout(new BorderLayout());
+	name_pane.add(name_label, BorderLayout.LINE_START);
+	name_pane.add(name, BorderLayout.CENTER);
+
+	value_label.setBorder(BorderFactory.createEmptyBorder(0,0,0,5));
+
+	inner_value_pane.setLayout(new BorderLayout());
+	inner_value_pane.add(value_label, BorderLayout.LINE_START);
+	inner_value_pane.add(value, BorderLayout.CENTER);
+
+	value_pane.setBorder(BorderFactory.createEmptyBorder(5,10,5,10));
+	value_pane.setLayout(new BorderLayout());
+	value_pane.add(inner_value_pane, BorderLayout.NORTH);
+
+	first_pane.setLayout(new BorderLayout());
+	first_pane.add(first_method, BorderLayout.LINE_START);
+	first_pane.add(first_value, BorderLayout.CENTER);
+	first_pane.add(first_case, BorderLayout.LINE_END);
+
+	operator_pane.setLayout(new GridLayout(1,4));
+	operator_pane.add(operator_label);
+	operator_pane.add(none_radiobutton);
+	operator_pane.add(and_radiobutton);
+	operator_pane.add(or_radiobutton);
+
+	second_pane.setLayout(new BorderLayout());
+	second_pane.add(second_method, BorderLayout.LINE_START);
+	second_pane.add(second_value, BorderLayout.CENTER);
+	second_pane.add(second_case, BorderLayout.LINE_END);
+
+	order_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	order_pane.setLayout(new GridLayout(1,2));
+	order_pane.add(order_label);
+	order_pane.add(ascending_radiobutton);
+	order_pane.add(descending_radiobutton);
+
+	custom_pane.setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
+	custom_pane.setLayout(new GridLayout(3,1));
+	custom_pane.add(first_pane);
+	custom_pane.add(operator_pane);
+	custom_pane.add(second_pane);
+
+	control.add(Dictionary.get("Autofilter.Filter_By_Value"), value_pane);
+	control.add(Dictionary.get("Autofilter.Custom_Filter"), custom_pane);
+
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	button_pane.setLayout(new GridLayout(1, 3));
+	button_pane.add(set_button);
+	button_pane.add(remove_button);
+	button_pane.add(cancel_button);
+
+	lower_pane.setLayout(new BorderLayout());
+	lower_pane.add(order_pane, BorderLayout.CENTER);
+	lower_pane.add(button_pane, BorderLayout.SOUTH);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(name_pane, BorderLayout.NORTH);
+	content_pane.add(control, BorderLayout.CENTER);
+	content_pane.add(lower_pane, BorderLayout.SOUTH);
+
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	screen_size = null;
+    }
+
+    public boolean cancelled() {
+	return cancelled;
+    }
+
+    /** Destructor. */
+    public void destroy() {
+	dispose();
+	first_case = null;
+	second_case = null;
+	first_method = null;
+	first_value = null;
+	second_method = null;
+	second_value = null;
+	value = null;
+	control = null;
+	dialog = null;
+	self = null;
+	name = null;
+    }
+    /** Display the modal dialog box, allowing the user to define the filter. When the user presses one of the buttons, dispose and return to the caller providing an indication of which button was pressed.
+     * @return An <i>int</i> which indicates which button was pressed to dismiss the dialog.
+     */
+    public Autofilter display(Autofilter filter, ArrayList raw_values, String column_name) {
+	this.cancelled = false;
+	this.filter = filter;
+	name.setText(column_name);
+	// Prune values so it only contains unique entries, then order.
+	TreeSet values = new TreeSet(raw_values);
+	value.setModel(new DefaultComboBoxModel(values.toArray()));
+	String star = "*";
+	value.insertItemAt(star, 0);
+	value.setSelectedItem(star);
+	first_value.setModel(new DefaultComboBoxModel(values.toArray()));
+	second_value.setModel(new DefaultComboBoxModel(values.toArray()));
+	// Restore previous values.
+	if(filter != null && filter.value_one != null) {
+	    value.setSelectedItem(filter.value_one);
+	    first_method.setSelectedIndex(filter.method_one);
+	    first_value.setSelectedItem(filter.value_one);
+	    first_case.setSelected(filter.casesense_one);
+	    if (filter.value_two == null) {
+		none_radiobutton.setSelected(true);
+	    }
+	    else {
+		if (filter.operation) {
+		    and_radiobutton.setSelected(true);
+		}
+		else {
+		    or_radiobutton.setSelected(true);
+		}
+		second_method.setSelectedIndex(filter.method_two);
+		second_value.setSelectedItem(filter.value_two);
+		second_case.setSelected(filter.casesense_two);
+	    }
+	    if (filter.sort) {
+		ascending_radiobutton.setSelected(true);
+		// order.setSelectedIndex(0);
+	    }
+	    else {
+		descending_radiobutton.setSelected(true);
+		// order.setSelectedIndex(1);
+	    }
+	}
+	// Display	  
+	setVisible(true);
+	dialog.toFront();
+	return this.filter;
+    }
+
+    /** Listens for actions on the button it is attached to, and when notified sets the return_value and disposes of the dialog. */
+    private final class ButtonListener
+	implements ActionListener {
+	private boolean cancel_button;
+	private boolean return_filter;
+	
+	/** Default constructor. Returns any filter that was already set */
+	public ButtonListener() {
+	    this.cancel_button = true;
+	}
+
+	/** Does an action on this button cause a filter to be returned. */
+	/** Constructor takes an associated return value as an argument.
+	 * @param return_filter <i>true</i> if we update then return the filter, <i>false</i> to clear existing filter.
+	 */
+	public ButtonListener(boolean return_filter) {
+	    this.cancel_button = false;
+	    this.return_filter = return_filter;
+	}
+	/** When any registered component is actioned apon, set the value and hide the dialog. We hide rather than dispose, because hide assures the data values will be retained. 
+	 * @param event An <strong>ActionEvent</strong> containing information about the action that caused this method call.
+	 * @see org.greenstone.gatherer.gui.metaaudit.Autofilter
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    if(cancel_button) {
+		cancelled = true;
+	    }
+	    else if(return_filter) {
+		if(control.getSelectedIndex() == 0) {
+		    filter.setFilter(1, 0, (String)value.getSelectedItem(), true);
+		    filter.setFilter(2, 0, null, true);
+		}
+		else {
+		    filter.setFilter(1, first_method.getSelectedIndex(), (String)first_value.getSelectedItem(), first_case.isSelected());
+		    if(!none_radiobutton.isSelected()) {
+			if (and_radiobutton.isSelected()) {
+			    filter.setOperation(Autofilter.AND);
+			}
+			else {
+			    filter.setOperation(Autofilter.OR);
+			}
+			filter.setFilter(2, second_method.getSelectedIndex(), (String)second_value.getSelectedItem(), second_case.isSelected());
+		    }
+		}
+		if (ascending_radiobutton.isSelected()) {
+		    filter.setSort(Autofilter.ASCENDING);
+		}
+		else {
+		    filter.setSort(Autofilter.DESCENDING);
+		}
+	    }
+	    else {
+		filter = null;
+	    }
+	    setVisible(false);
+	}
+    }
+    /** Listens for actions on the check box it is attached to, and when notified sets the state of the second method and value to the specified state. */
+    private final class CheckListener
+	implements ActionListener {
+	/** The default desire state any check button this listens to. */
+	private boolean desired_state = false;
+	/** The constructor takes an associated desired state.
+	 * @param desired_state The state that should be set when this is actioned, as a <i>boolean</i>.
+			*/
+	public CheckListener(boolean desired_state) {
+	    this.desired_state = desired_state;
+	}
+	/** Whenever our registered components are actioned apon, set the state of the second method and value to the desired state. */
+	public void actionPerformed(ActionEvent event) {
+	    second_method.setEnabled(desired_state);
+	    second_value.setEnabled(desired_state);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/Filter.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/Filter.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/Filter.java	(revision 31635)
@@ -0,0 +1,151 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui.metaaudit;
+
+/**************************************************************************************
+ * Title:        Gatherer
+ * Description:  The Gatherer: a tool for gathering and enriching a digital collection.
+ * Company:      The University of Waikato
+ * Written:      03/09/02
+ * Revised:      04/10/02 - Commented
+ **************************************************************************************/
+import de.qfs.lib.gui.TableModelFilter;
+import de.qfs.lib.gui.TableModelFilterEvent;
+import de.qfs.lib.gui.TableModelFilterListener;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Vector;
+import javax.swing.table.TableModel;
+
+/** This class essentially manages the autofilters applied to the metaaudit table.
+ * @author John Thompson
+ * @version 2.3
+ */
+public class Filter
+    implements TableModelFilter {
+    /** An array of autofilters. */
+    private Autofilter filters[];
+    /** The registered TableModelFilterListeners. */
+    private Vector listeners = new Vector ();
+    /** Constructor. 
+     * @param columns The number of columns this Filter will be expected to filter, as an <i>int</i>. 
+     */
+    public Filter(int columns) {
+	filters = new Autofilter[columns];
+    }
+    /** Add a TableModelFilterListener to the TableModelFilter. 
+     * @param listener The <strong>TableModelFilterListener</strong> to add, if it isn't already registered.
+     */
+    public synchronized void addTableModelFilterListener(TableModelFilterListener listener) {
+	if (!listeners.contains(listener)) {
+	    listeners.addElement(listener);
+	}
+    }
+    /** Remove a filter from a column.
+     * @param column The column number to remove the filter from, as an <i>int</i>.
+     */
+    public void clearFilter(int column) {
+	if(filters[column] != null) {
+	    filters[column].setActive(false);
+	}
+    }
+
+
+    /** Determine if a certain row should be shown in the table by checking it against all the current filters.
+     * @param model The <strong>TableModel</strong> the row is from.
+     * @param row The row number as an <i>int</i>
+     * @return <i>true</i> if the rows data matches all autofilters set and should be displayed, <i>false</i> otherwise.
+     */
+    public boolean filter(TableModel model, int row)
+    {
+	for (int i = 0; i < filters.length; i++) {
+	    if (filters[i] != null && filters[i].active()) {
+		ArrayList values = (ArrayList) model.getValueAt(row, i);
+		if (filters[i].filter(values) == false) {
+		    return false;
+		}
+	    }
+	}
+
+	return true;
+    }
+
+
+    /** Retrieve the autofilter associated with a certain column.
+     * @param column The column number as an <i>int</i>.
+     * @return The <strong>Autofilter</strong> assigned to that column.
+     */
+    public Autofilter getFilter(int column) {
+	Autofilter filter = filters[column];
+	if(filter == null) {
+	    filter = new Autofilter();
+	    filters[column] = filter;
+	}
+	return filter;
+    }
+    /** Determine if a certain column is filtered.
+     * @param column The column number as an <i>int</i>.
+     * @return <i>true</i> if there is an active autofilter assigned to this column, <i>false</i> otherwise.
+     */
+    public boolean isFiltered(int column) {
+	boolean result = false;
+	if(filters[column] != null) {
+	    result = filters[column].active();
+	}
+	return result;
+    }
+    /** Remove a TableModelFilterListener from the TableModelFilter.
+     * @param listener The <strong>TableModelFilterListener</strong> to remove.
+     */
+    public synchronized  void removeTableModelFilterListener(TableModelFilterListener listener) {
+	listeners.removeElement(listener);
+    }
+    /** Called whenever the filters assigned changes significantly, thus prompting a reload of the table model data. */
+    public void fireFilterChanged() {
+	///ystem.err.println("Filter Changed!");
+	Vector tmp;
+	synchronized(this) {
+	    tmp = (Vector) listeners.clone();
+	}
+		  
+	TableModelFilterEvent event = new TableModelFilterEvent (this);
+	Enumeration enumeration = tmp.elements();
+	while (enumeration.hasMoreElements()) {
+	    ((TableModelFilterListener) enumeration.nextElement()).filterChanged(event);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/HeaderListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/HeaderListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/HeaderListener.java	(revision 31635)
@@ -0,0 +1,79 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui.metaaudit;
+
+
+import de.qfs.lib.gui.TableModelSorter;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import org.greenstone.gatherer.metadata.MetadataAuditTableModel;
+
+public class HeaderListener
+    extends MouseAdapter {
+    private MetaAuditFrame parent_frame;
+    private MetaAuditTable table;
+    public HeaderListener(MetaAuditFrame parent_frame, MetaAuditTable table) {
+	this.parent_frame = parent_frame;
+	this.table = table;
+    }
+
+    public void mouseClicked(MouseEvent event) {
+	// Determine the column this was clicked on.
+	int column = table.getColumnModel().getColumnIndexAtX(event.getX());
+	int clicked_column = table.convertColumnIndexToModel(column);
+	// Display the currently assigned filter.
+	Filter filter_model = table.getFilter();
+	Autofilter filter = filter_model.getFilter(clicked_column);
+	MetadataAuditTableModel model = table.getOriginalModel();
+	ArrayList default_values = model.getColumnValues(clicked_column);
+	String column_name = model.getColumnName(clicked_column);
+	filter = parent_frame.autofilter_dialog.display(filter, default_values, column_name);
+	if(!parent_frame.autofilter_dialog.cancelled()) {
+	    if(filter == null) {
+		filter_model.clearFilter(clicked_column);
+	    }
+	    else {
+		filter.setActive(true);
+		TableModelSorter sorter = table.getSorter();
+		sorter.setSortColumn(clicked_column);
+		sorter.setSortAscending(filter.sort);
+	    }
+	    filter_model.fireFilterChanged();
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/HeaderRenderer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/HeaderRenderer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/HeaderRenderer.java	(revision 31635)
@@ -0,0 +1,79 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui.metaaudit;
+
+import java.awt.*;
+import javax.swing.*;
+import javax.swing.table.TableCellRenderer;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.util.JarTools;
+
+/** This custom button renderer replaces the boring old column headers with clickable versions which are based on JToggleButtons.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class HeaderRenderer 
+    extends JToggleButton
+    implements TableCellRenderer {
+
+    /** The constructor sets up a margin around the button, and adds the filter icons.
+     */
+    public HeaderRenderer() {
+	setMargin(new Insets(0,0,0,0));
+	setIcon(JarTools.getImage("filter.gif"));
+	setSelectedIcon(JarTools.getImage("filter-on.gif")); 
+    }
+
+    /** Called to create the component to be used as the 'rubber-stamp' for the table cell given the follwing arguments.
+     * @param table The <strong>JTable</strong> that is asking the renderer to draw; can be <i>null</i>.
+     * @param value The value of the cell to be rendered as an <strong>Object</strong>. It is up to the specific renderer to interpret and draw the value. For example, if value is the string "true", it could be rendered as a string or it could be rendered as a check box that is checked. null is a valid value.
+     * @param isSelected <i>true</i> if the cell is to be rendered with the selection highlighted; otherwise <i>false</i>.
+     * @param hasFocus If <i>true</i>, render cell appropriately. For example, put a special border on the cell, if the cell can be edited, render in the color used to indicate editing.
+     * @param row The row index of the cell being drawn as an <i>int</i>. When drawing the header, the value of row is -1.
+     * @param column The column index of the cell being drawn as an <i>int</i>.
+     * @see org.greenstone.gatherer.gui.metaaudit.MetaAuditTable
+     */
+    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
+	setText(value.toString());
+	setToolTipText(Dictionary.get("Autofilter.Table_Header_Tooltip", value.toString()));
+	MetaAuditTable temp = (MetaAuditTable) table;
+	if (temp != null) { // And it may if this is called during table init.
+	    setSelected(temp.isFiltered(column));
+	}
+	return this;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/MetaAuditFrame.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/MetaAuditFrame.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/MetaAuditFrame.java	(revision 31635)
@@ -0,0 +1,216 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui.metaaudit;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.SimpleMenuBar;
+import org.greenstone.gatherer.gui.ModalDialog;
+
+
+/** The MetaAuditFrame provides a table view of all of the metadata assigned to a selection of CollectionTreeNodes. All values for a certain file and a certain metadata element appear in the same cell. This table can be sorted by any column, and also has a MS Excel-like AutoFilter allowing you to restrict the rows visible to only those that match a certain set of criteria (applied to each column, and then 'ANDED', or cojoined, to determine the filter). Finally this dialog does not block the Gatherer tool, so the file selection can be changed and the dialog will just generate a new table dynamically.<BR>
+ * Much effort has gone into optimizing this table, as it quickly becomes slow and unresponsive to build/filter/sort when the number of records selected is high. However its performance is still nowhere as good as the Excel spreadsheet, and selections of 1000+ records can cause some serious waiting problems. Performance progression, shown in terms of time taken to perform action, are shown below. Note that building includes both the creation of the data model, and/or the time taken to lay out row and column sizes (due to this using multiple entry cells). Also all tables, by default, must be sorted by one column, which is initially the first column in ascending order:<BR><BR>
+ * <TABLE border=1 cellspacing=0 cellpadding=5>
+ * <TR><TD align=center colspan=5>Time Taken with 1000 records (ms)</TD></TR>
+ * <TR><TD>Action</TD><TD>Original</TD><TD>Revision 1</TD><TD>Revision 2</TD><TD>Revision 3</TD></TR>
+ * <TR><TD>Build</TD><TD align=right>113013</TD><TD align=right>1500</TD><TD align=right>1280</TD><TD align=right>665</TD></TR>
+ * <TR><TD>Sort</TD><TD align=right>34228</TD><TD align=right>353</TD><TD align=right>341</TD><TD align=right>338</TD></TR>
+ * <TR><TD>Filter</TD><TD align=right>57110</TD><TD align=right>485</TD><TD align=right>485</TD><TD align=right>485</TD></TR>
+ * </TABLE>
+ * <BR><BR>
+ * <TABLE border=1 cellspacing=0 cellpadding=5>
+ * <TR><TD align=center colspan=5>Time Taken with 10,000 records (ms) [hdlsub]</TD></TR>
+ * <TR><TD>Action</TD><TD>Original</TD><TD>Revision 1</TD><TD>Revision 2</TD><TD>Revision 3</TD></TR>
+ * <TR><TD>Build</TD><TD align=right>DNF</TD><TD align=right>8667</TD><TD align=right>6784</TD><TD align=right>3081</TD></TR>
+ * <TR><TD>Sort</TD><TD align=right>DNF</TD><TD align=right>236</TD><TD align=right>241</TD><TD align=right>228</TD></TR>
+ * <TR><TD>Filter</TD><TD align=right>DNF</TD><TD align=right>59272</TD><TD align=right>59767</TD><TD align=right>59614</TD></TR>
+ * </TABLE>
+ * <BR><BR>
+ * <TABLE border=1 cellspacing=0 cellpadding=5>
+ * <TR><TD align=center colspan=5>Time Taken with 30,000 records (ms) [hdl]</TD></TR>
+ * <TR><TD>Action</TD><TD>Original</TD><TD>Revision 1</TD><TD>Revision 2</TD><TD>Revision 3</TD></TR>
+ * <TR><TD>Build</TD><TD align=right>DNF</TD><TD align=right>19037</TD><TD align=right>16478</TD><TD align=right>8111</TD></TR>
+ * <TR><TD>Sort</TD><TD align=right>DNF</TD><TD align=right>314</TD><TD align=right>301</TD><TD align=right>385</TD></TR>
+ * <TR><TD>Filter</TD><TD align=right>DNF</TD><TD align=right>[300000]</TD><TD align=right>[300000]</TD><TD align=right>[300000]</TD></TR>
+ * </TABLE><BR><BR>
+ * It is easy to see that while building and sorting have become quite fast and reasonable, sorting is still unacceptable in terms of performance. The difficulty is this, that while sorting is easy (and fast) to implement via the Decorator pattern (making the overall table model a MDVC one) it requires the entire contents of a column to be available. Thus no matter the speed of your sort algorithm you will end up making N calls to getValueAt() in the table data model, for N records. This is not a problem for the initial model, hence the small sort times. However when you attempt to place another deocrator model (or modify the existing one) in order to filter only selected rows, even if the filtering itself takes little time, this little time quickly grows to unacceptable levels. In fact testing has shown it to grow much faster than linear time (though I'm not sure where). Doubling the number of records to filter increases the processing time by 5-7 times.<BR>
+ * In attempting to solve this problem, I have found many resources that mention a similar problem when trying to build visual representations of database tables, especially over networks or other temporally-non-deterministic connections. The common solution is to 'page' only those records necessary into memory on demand, thus making initial response significantly faster, while paying a small price for subsequent page seeks. There are even studies and algorithms for determining pages loads etc, I would imagine garnered from the memory management community. However none of these really help me usless I can get around the problem of having to have the entire columns population immeditaly accessable for sorting purposes.<BR>
+ * I believe with more time is would be possible to make a hybrid table model with properties such as you would require for the aforemenetioned database application, but which also provides fast methods for determining the highest and lowest records perhaps by building traversable heaps in the original model. Such a solution would incur a build penalty, and could consume significant memory, but would provide ordered and sequential access to the data within.<BR>
+ * The discussion aside, I have run out of time to do anything further on this, so will include as is, and provide a warning dialog whenever the number of records might suffer slow processing (1000+ records).
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class MetaAuditFrame
+    extends ModalDialog 
+    implements TreeSelectionListener {
+    public AutofilterDialog autofilter_dialog;
+    /** Whether the selection has changed since the last time we built the model (because it has been hidden and theres no point refreshing a model you can't see!). */
+    private boolean invalid = true;
+    /** An array holding the most recent list of paths selected (plus some nulls for those paths removed). */
+    private CollectionTreeNode[] records = null;
+    /** A reference to ourselves so that inner classes can interact with us. */
+    private MetaAuditFrame self;
+    /** The table in which the metadata is shown. */
+    private MetaAuditTable table;
+    /** The default size for the metaaudit dialog. */
+    static final private Dimension SIZE = new Dimension(640,505);
+
+
+    public MetaAuditFrame()
+    {
+	super(Gatherer.g_man);
+        
+	// Arguments
+	this.autofilter_dialog = new AutofilterDialog(this);
+	this.self = this;
+	this.table = new MetaAuditTable(this);
+        this.setComponentOrientation(Dictionary.getOrientation());
+	// Creation
+	setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
+	setSize(SIZE);
+	setTitle(Dictionary.get("MetaAudit.Title"));
+	setJMenuBar(new SimpleMenuBar("reviewingmetadata")); 
+	
+	JPanel content_pane = (JPanel) getContentPane();
+        content_pane.setComponentOrientation(Dictionary.getOrientation());
+	JPanel button_pane = new JPanel();
+        button_pane.setComponentOrientation(Dictionary.getOrientation());
+        
+	JButton close_button = new GLIButton(Dictionary.get("MetaAudit.Close"), Dictionary.get("MetaAudit.Close_Tooltip"));
+	
+	// Connection
+	close_button.addActionListener(new CloseListener());
+	Gatherer.c_man.getCollectionTree().addTreeSelectionListener(this);
+
+	// Layout
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	button_pane.setLayout(new BorderLayout());
+	button_pane.add(close_button, BorderLayout.CENTER);
+		 
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(new JScrollPane(table), BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+		  
+	Dimension screen_size = Configuration.screen_size;
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	screen_size = null;
+	close_button = null;
+	button_pane = null;
+	content_pane = null;
+    }
+
+    /** Destructor. */
+    public void destroy() {
+	records = null;
+	self = null;
+	table = null;
+    }
+
+    /** Display the dialog on screen. */
+    public void display() {
+	if(invalid) {
+	    rebuildModel();
+	}
+	setVisible(true);
+    }
+
+    /** This method is called whenever the selection within the collection tree changes. This causes the table to be rebuilt with a new model.
+     * @param event A <strong>TreeSelectionEvent</strong> containing information about the event.
+     */
+    public void valueChanged(TreeSelectionEvent event) {
+	Object source = event.getSource();
+	if(source instanceof JTree) {
+	    TreePath paths[] = ((JTree)source).getSelectionPaths();
+	    if(paths != null) {
+		records = new CollectionTreeNode[paths.length];
+		for(int i = 0; i < paths.length; i++) {
+		    records[i] = (CollectionTreeNode) paths[i].getLastPathComponent();
+		}
+		if(isVisible()) {
+		    rebuildModel();
+		}
+		else {
+		    invalid = true;
+		}
+	    }
+	}
+    }
+
+    public void wait(boolean waiting) {
+	Component glass_pane = getGlassPane();
+	if(waiting) {
+	    // Show wait cursor.
+	    glass_pane.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+	    glass_pane.setVisible(true);
+	}
+	else {
+	    // Hide wait cursor.
+	    glass_pane.setVisible(false);
+	    glass_pane.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+	}
+	glass_pane = null;
+    }
+
+    /** Rebuild the metaaudit table model using the current collection tree selection. 
+     */
+    private void rebuildModel() {
+	// Build and set model
+	table.newModel(records);
+	// Done.
+	invalid = false;
+    }
+
+    /** Listens for actions upon the close button, and if detected closes the dialog */
+    private class CloseListener
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action has been performed on our target control(s).
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    self.setVisible(false);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/MetaAuditRenderer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/MetaAuditRenderer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/MetaAuditRenderer.java	(revision 31635)
@@ -0,0 +1,63 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui.metaaudit;
+
+import java.awt.*;
+import java.util.ArrayList;
+import javax.swing.*;
+import javax.swing.table.DefaultTableCellRenderer;
+
+public class MetaAuditRenderer
+    extends DefaultTableCellRenderer {
+
+    public Component getTableCellRendererComponent(JTable table, Object value, boolean is_selected, boolean has_focus, int row, int column) {
+	ArrayList array = (ArrayList) value;
+	JLabel label = (JLabel) super.getTableCellRendererComponent(table, "", is_selected, has_focus, row, column);
+	JPanel panel = new JPanel();
+	panel.setBackground(label.getBackground());
+	panel.setBorder(label.getBorder());
+	if(array != null) {
+	    panel.setLayout(new GridLayout(array.size(), 1));
+	    for(int i = 0; i < array.size(); i++) {
+		JLabel entry = new JLabel(array.get(i).toString());
+		entry.setFont(label.getFont());
+		panel.add(entry);
+	    }
+	}
+	return panel;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/MetaAuditTable.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/MetaAuditTable.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/metaaudit/MetaAuditTable.java	(revision 31635)
@@ -0,0 +1,200 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui.metaaudit;
+
+import de.qfs.lib.gui.*;
+import java.awt.*;
+import java.util.ArrayList;
+import javax.swing.JTable;
+import javax.swing.event.TableModelEvent;
+import javax.swing.table.*;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.metadata.MetadataAuditTableModel;
+
+
+/**
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class MetaAuditTable
+    extends JTable 
+    implements TableModelSorterListener {
+
+    private boolean initial_state;
+
+    private Filter filter;
+
+    private HeaderRenderer header_renderer;
+
+    private MetaAuditFrame parent_frame;
+
+    private MetaAuditRenderer cell_renderer;
+
+    private MetadataAuditTableModel metadata_audit_table_model;
+
+    private SortedTableHelper helper;
+
+    static final private int MARGIN = 10;
+
+    public MetaAuditTable(MetaAuditFrame parent_frame) {
+	super();
+        this.setComponentOrientation(Dictionary.getOrientation());
+	this.cell_renderer = new MetaAuditRenderer();
+	this.header_renderer = new HeaderRenderer();
+	this.parent_frame = parent_frame;
+	getTableHeader().addMouseListener(new HeaderListener(parent_frame, this));
+	setAutoResizeMode(AUTO_RESIZE_OFF);
+    }
+
+    public Filter getFilter() {
+	return filter;
+    }
+
+
+   public MetadataAuditTableModel getOriginalModel()
+   {
+      return metadata_audit_table_model;
+   }
+
+
+    public TableModelSorter getSorter() {
+	return helper.getTableModelSorter();
+    }
+
+    public boolean isFiltered(int column) {
+	if(filter == null) {
+	    return false;
+	}
+	return filter.isFiltered(column);
+    }
+
+    public void newModel(CollectionTreeNode records[]) {
+	if(records == null || records.length == 0) {
+	    setModel(new DefaultTableModel());
+	    return;
+	}
+	wait(true);
+	initial_state = true;
+	// System.err.println("Build new Model - should only be called once per click!");
+	metadata_audit_table_model = new MetadataAuditTableModel();
+	metadata_audit_table_model.rebuild(records);
+	setModel(metadata_audit_table_model);
+	filter = new Filter(metadata_audit_table_model.getColumnCount());
+	helper = new SortedTableHelper (this, filter, new DefaultTableModelSorter(), header_renderer, null);
+	helper.prepareTable();
+	// Add renderer to columns.
+	TableColumnModel column_model = getColumnModel();
+	for(int i = 0; i < column_model.getColumnCount(); i++) {
+	    column_model.getColumn(i).setCellRenderer(cell_renderer);
+	    column_model.getColumn(i).setHeaderRenderer(header_renderer);
+	}
+	wait(false);
+	initial_state = false;
+    }
+
+    public void tableChanged(TableModelEvent e) {
+	wait(true);
+	super.tableChanged(e);
+
+	// Create storage area
+	ArrayList heights[] = new ArrayList[dataModel.getRowCount()];
+	int widths_value[] = new int[dataModel.getColumnCount()];
+	ArrayList widths[] = new ArrayList[dataModel.getColumnCount()];
+
+	// Iterate through cell values finding tallest and widest for each row and column respectively.
+	for (int j = 0; j < dataModel.getRowCount(); j++) {
+	    for (int k = 0; k < dataModel.getColumnCount(); k++) {
+		Object object = dataModel.getValueAt(j, k);
+		if (object instanceof ArrayList) {
+		    ArrayList data = (ArrayList) object;
+		    if (heights[j] == null || data.size() > heights[j].size()) {
+			heights[j] = data;
+		    }
+		    String longest = "";
+		    for (int i = 0; i < data.size(); i++) {
+			String temp = data.get(i).toString();
+			if (temp.length() > longest.length()) {
+			    longest = temp;
+			}
+		    }
+		    if (widths[k] == null || longest.length() > widths_value[k]) {
+			widths_value[k] = longest.length();
+			widths[k] = data;
+		    }
+		}
+	    }
+	}
+
+	// Now assign the dimensions we have determined to be the largest.
+	for (int l = 0; l < heights.length; l++) {
+	    if (heights[l] != null) {
+		Component component = cell_renderer.getTableCellRendererComponent(this, heights[l], false, false, 0, 0);
+		Dimension size = component.getPreferredSize();
+		setRowHeight(l, size.height);
+	    }
+	}
+	for (int m = 0; m < widths.length; m++) {
+	    if (widths[m] != null) {
+		Component component = cell_renderer.getTableCellRendererComponent(this, widths[m], false, false, 0, 0);
+		Dimension size = component.getPreferredSize();
+		int width = size.width + MARGIN;
+		if (width > (2 * parent_frame.getSize().width) / 3) {
+		    width = (2 * parent_frame.getSize().width) / 3;
+		}
+
+		Component header = header_renderer.getTableCellRendererComponent(this, dataModel.getColumnName(m), false, false, 0, 0);
+		if (header.getPreferredSize().width + MARGIN > width) {
+		    width = header.getPreferredSize().width + MARGIN;
+		}
+		columnModel.getColumn(m).setPreferredWidth(width);
+	    }
+	}
+
+	wait(initial_state);
+    }
+
+
+    public void sortOrderChanged(TableModelSorterEvent event) {
+    }
+
+    private void wait(boolean waiting) {
+	if(parent_frame != null) {
+	    parent_frame.wait(waiting);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/tree/DragTree.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/tree/DragTree.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/tree/DragTree.java	(revision 31635)
@@ -0,0 +1,820 @@
+package org.greenstone.gatherer.gui.tree;
+
+import java.awt.*;
+import java.awt.datatransfer.*;
+import java.awt.dnd.*;
+import java.awt.event.*;
+import java.awt.geom.AffineTransform;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.plaf.basic.BasicTreeUI;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.file.FileNode;
+import org.greenstone.gatherer.file.FileSystemModel;
+import org.greenstone.gatherer.gui.Filter;
+import org.greenstone.gatherer.util.DragComponent;
+import org.greenstone.gatherer.util.DragGroup;
+import org.greenstone.gatherer.util.DragTreeSelectionModel;
+import org.greenstone.gatherer.util.Utility;
+
+public abstract class DragTree
+    extends JTree
+    implements Autoscroll, DragGestureListener, DragSourceListener, DropTargetListener, DragComponent, TreeSelectionListener {
+    /** The normal background color. */
+    private Color background_color;
+    /** The normal foreground color. */
+    private Color foreground_color;
+    /** The Group this component belongs to. */
+    private DragGroup group;
+    /** The filter for this tree. */
+    protected Filter filter = null;
+    /** The image to use for the disabled background. */
+    private ImageIcon disabled_background;
+    /** The image to use for a normal background. */
+    private ImageIcon normal_background;
+    /** The icon to use for multiple node drag'n'drops. We decided against using the windows paradigm or a block of x horizontal lines for x files. */
+    private ImageIcon multiple_icon = new ImageIcon("resource"+File.separator+"multiple.gif");
+    /** The default drag action, although its not that important as we provide custom icons during drags. */
+    private int drag_action = DnDConstants.ACTION_MOVE;
+    /** The location of the last ghost drawn, so that we can repair the 'spoilt' area. */
+    private Point pt_last = null;
+    /** The region borderer by the lower cue line. */
+    private Rectangle lower_cue_line;
+    /** The region covered by the drag ghost icon. */
+    private Rectangle ra_ghost = new Rectangle();
+    /** The region borderer by the upper cue line. */
+    private Rectangle upper_cue_line;
+    /** The last tree path the drag was hovered over. */
+    private TreePath previous_path = null;
+
+    static private Cursor NO_DRAG_CURSOR = null;
+
+    static private final Color TRANSPARENT_COLOR = new Color(0,0,0,0);
+    /** The distance from the edge of the current view within the scroll bar which if entered causes the view to scroll. */
+    static private final int AUTOSCROLL_MARGIN = 12;
+
+    static public final int TREE_DISPLAY_CHANGED        = 0;
+    static public final int LOADED_COLLECTION_CHANGED   = 1;
+    static public final int COLLECTION_CONTENTS_CHANGED = 2;
+
+
+    public DragTree(TreeModel model, boolean mixed_selection)
+    {
+	super();
+
+	// For some reason these aren't set with Java 1.4.2 and the GTK look and feel?
+	if (UIManager.get("Tree.leftChildIndent") == null) {
+	    UIManager.put("Tree.leftChildIndent", new Integer(7));
+	}
+	if (UIManager.get("Tree.rightChildIndent") == null) {
+	    UIManager.put("Tree.rightChildIndent", new Integer(13));
+	}
+
+	init(mixed_selection);
+	if (model != null) {
+	    setModel(model);
+	}
+    }
+
+    public void init(boolean mixed_selection) {
+	if (NO_DRAG_CURSOR == null) {
+	    NO_DRAG_CURSOR = DragSource.DefaultMoveNoDrop;
+	}
+
+	// Init
+	this.filter = new Filter(this, null);
+
+	// Creation
+	this.putClientProperty("JTree.lineStyle", "Angled");
+	this.setAutoscrolls(true);
+	this.setEditable(false);
+	this.setLargeModel(true);
+	this.setOpaque(true);
+	this.setRootVisible(false);
+	this.setSelectionModel(new DragTreeSelectionModel(this, mixed_selection));
+	this.setShowsRootHandles(true);
+	this.setUI(new BasicTreeUI());
+
+	// Connection
+	addKeyListener(new DragTreeKeyListener());
+	addMouseListener(Gatherer.g_man.foa_listener);
+	addTreeExpansionListener(Gatherer.g_man.foa_listener);
+	addTreeSelectionListener(this);
+
+	DragTreeCellRenderer renderer = new DragTreeCellRenderer();
+	//make the renderer paint nodes as transparent when not selected
+	//renderer.setBackgroundNonSelectionColor(new Color(0,0,0,0));
+	setCellRenderer(renderer);
+	// It turns out VariableHeightLayoutCache is buggy under MacOS, so I'll force it to use FixedHeightLayoutCache. To do that I have to set a cell height, ala below, and set the large model property to true, ala above. And buggy VariableHeightLayoutCache goes away. Plus this actually provides a minor performance boost as the layout manager doesn't have to calculate new bounds each time (well... I suppose any gain is actually pretty much eclipsed by the time taken for file access while we determine what a certain nodes children are).
+	// And once we have the cell renderer, use it to determine a fixed height for the rows
+	Component prototype_row = renderer.getTreeCellRendererComponent(this, "Prototype", true, true, false, 0, true);
+	this.setRowHeight(prototype_row.getSize().height);
+	prototype_row = null;
+
+
+	// Drag'n'drop Setup
+	// Drag source setup.
+	DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(this, drag_action, this);
+	// Drop destination setup.
+	new DropTarget(this, drag_action, this, true);
+    }
+
+    // Autoscroll Interface - Scroll because the mouse cursor is in our scroll zone.<br>
+    // The following code was borrowed from the book:<br>
+    //              Java Swing<br>
+    //              By Robert Eckstein, Marc Loy & Dave Wood<br>
+    //              Paperback - 1221 pages 1 Ed edition (September 1998)<br>
+    //              O'Reilly & Associates; ISBN: 156592455X<br>
+    // The relevant chapter of which can be found at:<br>
+    //              http://www.oreilly.com/catalog/jswing/chapter/dnd.beta.pdf<br>
+    // But I've probably tortured it beyond all recognition anyway.
+    public void autoscroll(Point pt) {
+	// Figure out which row we're on.
+	int row = getRowForLocation(pt.x, pt.y);
+	// If we are not on a row then ignore this autoscroll request
+	if (row < 0) return;
+	Rectangle bounds = getBounds();// Yes, scroll up one row
+	// Now decide if the row is at the top of the screen or at the bottom. We do this to make the previous row (or the next row) visible as appropriate. If we're at the absolute top or bottom, just return the first or last row respectively.
+	// Is row at top of screen?
+	if(pt.y + bounds.y <= AUTOSCROLL_MARGIN) {
+	    // Yes, scroll up one row
+	    if(row <= 0) {
+		row = 0;
+	    }
+	    else {
+		row = row - 1;
+	    }
+	}
+	else {
+	    // No, scroll down one row
+	    if(row < getRowCount() - 1) {
+		row = row + 1;
+	    }
+	}
+	this.scrollRowToVisible(row);
+    }
+
+    /** In order for the appearance to be consistant, given we may be in the situation where the pointer has left our focus but the ghost remains, this method allows other members of the GGroup to tell this component to clear its ghost.
+     */
+    public void clearGhost() {
+	// Erase the last ghost image and cue line
+	paintImmediately(ra_ghost.getBounds());
+    }
+
+    /** Any implementation of DragSourceListener must include this method so we can be notified when the drag event ends (somewhere else), which will in turn remove actions.
+     * @param event A <strong>DragSourceDropEvent</strong> containing all the information about the end of the drag event.
+     */
+    public void dragDropEnd(DragSourceDropEvent event) {
+    }
+
+    /** Any implementation of DragSourceListener must include this method so we can be notified when the drag focus enters this component.
+     * @param event A <strong>DragSourceDragEvent</strong> containing all the information
+     * about the drag event.
+     */
+    public void dragEnter(DragSourceDragEvent event) {
+	// Handled elsewhere.
+    }
+    /** Any implementation of DropTargetListener must include this method so we can be notified when the drag focus enters this component, which in this case is to grab focus from within our group.
+     * @param event A <strong>DropTargetDragEvent</strong> containing all the information about the drag event.
+     */
+    public void dragEnter(DropTargetDragEvent event) {
+	group.grabFocus(this);
+    }
+    /** Any implementation of DragSourceListener must include this method so we can be notified when the drag focus leaves this component.
+     * @param event A <strong>DragSourceEvent</strong> containing all the information about the drag event.
+     */
+    public void dragExit(DragSourceEvent event) {
+	clearGhost();
+    }
+
+    /** Any implementation of DropTargetListener must include this method
+     * so we can be notified when the drag focus leaves this component.
+     * @param event A DropTargetEvent containing all the information
+     * about the drag event.
+     */
+    public void dragExit(DropTargetEvent event) {
+	clearGhost();
+    }
+
+    /** Any implementation of DragGestureListener must include this method
+     * so we can be notified when a drag action has been noticed, thus a
+     * drag action has begun.
+     * @param event A DragGestureEvent containing all the information about
+     * the drag event.
+     */
+    public void dragGestureRecognized(DragGestureEvent event)
+    {
+	// Check that the tree is draggable
+	if (!isDraggable()) {
+	    return;
+	}
+
+	// Disable editing, unless you want to have the edit box pop-up part way through dragging.
+	this.setEditable(false);
+	// We need this to find one of the selected nodes.
+	Point origin = event.getDragOrigin();
+	TreePath path = this.getPathForLocation(origin.x, origin.y);
+	// Taking into account our delayed model of selection, it is possible the user has performed a select and drag in one click. Here we utilize the Windows paradigm like so: If the node at the origin of the drag and drop is already in our selection then we perform multiple drag and drop. Otherwise we recognise that this is a distinct drag-drop and move only the origin node.
+	if(!isPathSelected(path)) {
+	    ((DragTreeSelectionModel)selectionModel).setImmediate(true);
+	    setSelectionPath(path);
+	    ((DragTreeSelectionModel)selectionModel).setImmediate(false);
+	}
+	if(path == null) {
+	    return;
+	}
+	if (!isValidDrag()) {
+	    try {
+		event.startDrag(NO_DRAG_CURSOR, new StringSelection("dummy"), this);
+		//dragging = true;
+	    }
+	    catch(Exception error) {
+		error.printStackTrace();
+	    }
+	    return;
+	}
+	// Now update the selection stored as far as the group is concerned.
+	group.setSelection(getSelectionPaths());
+	group.setSource(this);
+	// First grab ghost.
+	group.grabFocus(this);
+	// Ghost Image stuff.
+	Rectangle rect = this.getPathBounds(path);
+	group.mouse_offset = new Point(origin.x - rect.x, origin.y - rect.y);
+	// Create the ghost image.
+	// Retrieve the selected files.
+	int selection_count = getSelectionCount();
+	if(selection_count > 0) {
+	    JLabel label;
+	    if(selection_count == 1) {
+		TreePath node_path = getSelectionPath();
+		FileNode node = (FileNode) path.getLastPathComponent();
+		label = new JLabel(node.toString(), ((DefaultTreeCellRenderer)getCellRenderer()).getLeafIcon(), JLabel.CENTER);
+	    }
+	    else {
+		String title = getSelectionCount() + " " + Dictionary.get("Tree.Files");
+		label = new JLabel(title, ((DefaultTreeCellRenderer)getCellRenderer()).getClosedIcon(), JLabel.CENTER);
+		title = null;
+	    }
+	    // The layout manager normally does this.
+	    Dimension label_size = label.getPreferredSize();
+	    label.setSize(label_size);
+	    label.setBackground(TRANSPARENT_COLOR);
+	    label.setOpaque(true);
+	    // Get a buffered image of the selection for dragging a ghost image.
+	    group.image_ghost = new BufferedImage(label_size.width, label_size.height, BufferedImage.TYPE_INT_ARGB_PRE);
+	    label_size = null;
+	    // Get a graphics context for this image.
+	    Graphics2D g2 = group.image_ghost.createGraphics();
+	    // Make the image ghostlike
+	    g2.setComposite(AlphaComposite.getInstance (AlphaComposite.SRC, 0.5f));
+	    // Ask the cell renderer to paint itself into the BufferedImage
+	    label.paint(g2);
+	    g2 = null;
+	    label = null;
+	    try {
+		event.startDrag(new Cursor(Cursor.DEFAULT_CURSOR), group.image_ghost, group.mouse_offset, new StringSelection("dummy"), this);
+		//dragging = true;
+	    }
+	    catch(Exception error) {
+		error.printStackTrace();
+	    }
+	}
+
+    }
+
+    /** Implementation side-effect.
+     * @param event A DragSourceDragEvent containing all the information about the drag event.
+     */
+    public void dragOver(DragSourceDragEvent event) {
+    }
+
+    /** Any implementation of DropTargetListener must include this method
+     * so we can be notified when the drag moves in this component.
+     * @param event A DropTargetDragEvent containing all the information
+     * about the drag event.
+     */
+    public void dragOver(DropTargetDragEvent event) {
+	// Draw the mouse ghost
+	Graphics2D g2 = (Graphics2D) getGraphics();
+	Point pt = event.getLocation();
+	if(pt_last != null && pt.equals(pt_last)) {
+	    return;
+	}
+	pt_last = pt;
+	if(!DragSource.isDragImageSupported() && group.image_ghost != null) {
+	    // Erase the last ghost image and cue line
+	    paintImmediately(ra_ghost.getBounds());
+	    // Remember where you are about to draw the new ghost image
+	    ra_ghost.setRect(pt.x - group.mouse_offset.x, pt.y - group.mouse_offset.y, group.image_ghost.getWidth(), group.image_ghost.getHeight());
+	    // Draw the ghost image
+	    g2.drawImage(group.image_ghost, AffineTransform.getTranslateInstance(ra_ghost.getX(), ra_ghost.getY()), null);
+	}
+	// Now we highlight the target node if it is a valid drop target. Of course we don't bother if we are still over a node which has already been identified as to whether its a drop target
+	TreePath target_path = this.getPathForLocation(pt.x, pt.y);
+	
+	if (target_path == null) {
+	    // the user has moved the mouse into background area - need to remove any existing cue lines
+	    if(upper_cue_line != null && lower_cue_line != null) {
+		paintImmediately(upper_cue_line.getBounds());
+		paintImmediately(lower_cue_line.getBounds());
+		previous_path = null;
+	    }
+	    // don't need to display anything
+	    return;
+	} 
+	
+	if(previous_path == null || target_path != null && !target_path.equals(previous_path)) {
+	    // Immediately clear the old cue lines.
+	    if(upper_cue_line != null && lower_cue_line != null) {
+		paintImmediately(upper_cue_line.getBounds());
+		paintImmediately(lower_cue_line.getBounds());
+	    }
+	    if(isValidDrop(target_path)) {
+		// Get the drop target's bounding rectangle
+		Rectangle raPath = getPathBounds(target_path);
+		// Cue line bounds (2 pixels beneath the drop target)
+		upper_cue_line = new Rectangle(0, raPath.y + (int)raPath.getHeight(), getWidth(), 2);
+		lower_cue_line = new Rectangle(0, raPath.y, getWidth(), 2);
+		g2.setColor(((DefaultTreeCellRenderer)cellRenderer).getBackgroundSelectionColor()); // The cue line color
+		g2.fill(upper_cue_line);         // Draw the cue line
+		g2.fill(lower_cue_line);
+	    }
+	    else {
+		upper_cue_line = null;
+		lower_cue_line = null;
+	    }
+	}
+    }
+
+    /** Any implementation of DropTargetListener must include this method
+	  * so we can be notified when the drag ends, ie the transferable is
+	  * dropped.
+	  * @param event A DropTargetDropEvent containing all the information
+	  * about the end of the drag event.
+	  */
+    public void drop(DropTargetDropEvent event) {
+	///start = System.currentTimeMillis();
+	///ystem.err.println("Drop target drop: " + this);
+	if (!isDroppable()) {
+	    return;
+	}
+
+	event.acceptDrop(drag_action);
+
+	// Determine what node we dropped over.
+	Point pt = event.getLocation();
+	TreePath target_path = this.getPathForLocation(pt.x, pt.y);
+	FileNode target = null;
+	if(target_path != null) {
+	    if(isValidDrop(target_path)) {
+		target = (FileNode) target_path.getLastPathComponent();
+	    } else if (isValidDropOntoFile(target_path)) {
+		target = (FileNode) target_path.getParentPath().getLastPathComponent();
+	    }
+	}
+	else {
+	    TreeModel m = getModel();
+	    // check that we have a model and is a FileSystemModel (ie check that a collection is loaded
+	    if (m != null && m instanceof FileSystemModel) {
+		target = (FileNode) m.getRoot();
+	    }
+	}
+	target_path = null;
+	pt = null;
+	if(target != null) {
+	    ///ystem.err.println("Valid drop.");
+	    TreePath[] selection = group.getSelection();
+	    if(target != null && selection != null) {
+		FileNode[] source_nodes = new FileNode[selection.length];
+		for(int i = 0; i < source_nodes.length; i++) {
+		    source_nodes[i] = (FileNode) selection[i].getLastPathComponent();
+		}
+		Gatherer.f_man.action(group.getSource(), source_nodes, this, target);
+		source_nodes = null;
+	    }
+	    group.setSelection(null);
+	    group.setSource(null);
+	    selection = null;
+	    target = null;
+	}
+
+	// Clear up the group.image_ghost
+	paintImmediately(ra_ghost.getBounds());
+	event.getDropTargetContext().dropComplete(true);
+	group.image_ghost = null;
+    }
+
+
+    /** Any implementation of DragSourceListener must include this method
+     * so we can be notified when the action to be taken upon drop changes.
+     * @param event A DragSourceDragEvent containing all the information
+     * about the drag event.
+     */
+    public void dropActionChanged(DragSourceDragEvent event) {
+    }
+
+    /** Any implementation of DropTargetListener must include this method
+     * so we can be notified when the action to be taken upon drop changes.
+     * @param event A DropTargetDragEvent containing all the information
+     * about the drag event.
+     */
+    public void dropActionChanged(DropTargetDragEvent event) {
+    }
+
+    /** Used to notify this component that it has gained focus. It should
+     * make some effort to inform the user of this.
+     */
+    public void gainFocus() {
+	///ystem.err.println("Gained focus: " + this);
+	((DragTreeCellRenderer)cellRenderer).gainFocus();
+	repaint();
+    }
+
+    /** Autoscroll Interface...
+     * The following code was borrowed from the book:
+     *              Java Swing
+     *              By Robert Eckstein, Marc Loy & Dave Wood
+     *              Paperback - 1221 pages 1 Ed edition (September 1998)
+     *              O'Reilly & Associates; ISBN: 156592455X
+     *
+     * The relevant chapter of which can be found at:
+     *              http://www.oreilly.com/catalog/jswing/chapter/dnd.beta.pdf
+     * Calculate the insets for the *JTREE*, not the viewport
+     * the tree is in. This makes it a bit messy.
+     */
+    public Insets getAutoscrollInsets()
+    {
+	Rectangle raOuter = this.getBounds();
+	Rectangle raInner = this.getParent().getBounds();
+	return new Insets(raInner.y - raOuter.y + AUTOSCROLL_MARGIN,
+			  raInner.x - raOuter.x + AUTOSCROLL_MARGIN,
+			  raOuter.height - raInner.height - raInner.y + raOuter.y + AUTOSCROLL_MARGIN,
+			  raOuter.width - raInner.width - raInner.x + raOuter.x + AUTOSCROLL_MARGIN);
+    }
+
+    public Filter getFilter() {
+	return filter;
+    }
+
+    public String getSelectionDetails() {
+	return ((DragTreeSelectionModel)selectionModel).getDetails();
+    }
+
+    public FileSystemModel getTreeModel() {
+	return (FileSystemModel) getModel();
+    }
+
+
+    protected abstract boolean isDraggable();
+
+
+    protected abstract boolean isDroppable();
+
+
+    /** This method is used to inform this component when it loses focus,
+     * and should indicate this somehow.
+     */
+    public void loseFocus() {
+	///ystem.err.println("Lost focus: " + this);
+	((DragTreeCellRenderer)cellRenderer).loseFocus();
+	repaint();
+    }
+
+
+    public void paint(Graphics g) {
+	if(disabled_background != null) {
+	    int height = getSize().height;
+	    int offset = 0;
+	    ImageIcon background;
+	    if(isEnabled()) {
+		background = normal_background;
+	    }
+	    else {
+		background = disabled_background;
+	    }
+	    while((height - offset) > 0) {
+		g.drawImage(background.getImage(), 0, offset, null);
+		offset = offset + background.getIconHeight();
+	    }
+	    background = null;
+	}
+	super.paint(g);
+    }
+
+    public void refresh(TreePath path) {
+	if (treeModel instanceof FileSystemModel) {
+	    ((FileSystemModel)treeModel).refresh(path);
+	}
+	else {
+	    // System.err.println("DragTree::refresh - Tree model is " + treeModel);
+	}
+    }
+
+    public void setBackgroundNonSelectionColor(Color color) {
+	background_color = color;
+	if(isEnabled()) {
+	    setBackground(color);
+	    ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(color);
+	}
+	else {
+	    setBackground(Color.lightGray);
+	    ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(Color.lightGray);
+	}
+	repaint();
+    }
+
+    public void setBackgroundSelectionColor(Color color) {
+	((DefaultTreeCellRenderer)cellRenderer).setBackgroundSelectionColor(color);
+	repaint();
+    }
+
+    /** Override the normal setEnabled so the Tree exhibits a little more
+     * change, which in this instance is the background colour changing.
+     * @param state Whether this GTree should be in an enabled state.
+     */
+    public void setEnabled(boolean state) {
+	super.setEnabled(state);
+
+	// Change some colors
+	if(state) {
+	    setBackground(background_color);
+	    ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(background_color);
+	    ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(foreground_color);
+	}
+	else {
+	    setBackground(Color.lightGray);
+	    ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(Color.lightGray);
+	    ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(Color.black);
+	}
+	repaint();
+    }
+
+    public void setGroup(DragGroup group) {
+	this.group = group;
+    }
+
+    /** Determines whether the following selection attempts should go through the normal delayed selection model, or should happen immediately.*/
+    public void setImmediate(boolean state) {
+	((DragTreeSelectionModel)selectionModel).setImmediate(state);
+    }
+
+    public void setModel(TreeModel model) {
+	super.setModel(model);
+	if(model instanceof FileSystemModel) {
+	    FileSystemModel file_system_model = (FileSystemModel) model;
+	    file_system_model.setTree(this);
+	    removeTreeExpansionListener(file_system_model);
+	    addTreeExpansionListener(file_system_model);
+	    removeTreeWillExpandListener(file_system_model);
+	    addTreeWillExpandListener(file_system_model);
+	    file_system_model = null;
+	}
+    }
+
+    /** Ensure that that file node denoted by the given file is selected. */
+    public void setSelection(File file) {
+	// We know the file exists, and thus that it must exists somewhere in our tree hierarchy.
+	// 1. Retrieve the root node of our tree.
+	FileNode current = (FileNode) getModel().getRoot();
+	// 2. Find that node in the file parents, keeping track of each intermediate file.
+	ArrayList files = new ArrayList();
+	while(file != null && !current.toString().equals(file.getName())) {
+	    files.add(0, file);
+	    file = file.getParentFile();
+	}
+	if(file == null) {
+	    return;
+	}
+	// 3. While there are still remaining intermediate files.
+	while(files.size() > 0) {
+	    file = (File) files.remove(0);
+				// 3a. Find the next file in the current nodes children.
+	    boolean found = false;
+	    current.map();
+	    for(int i = 0; !found && i < current.getChildCount(); i++) {
+		FileNode child = (FileNode) current.getChildAt(i);
+		if(child.toString().equals(file.getName())) {
+		    // 3b. Make the current node this node (if found) and continue.
+		    found = true;
+		    current = child;
+		}
+	    }
+	    // 3c. If not found then return as this node can't exists somehow.
+	    if(!found) {
+		return;
+	    }
+	}
+	// 4. We should now have the desired node. Remember to make the selection immediate.
+	TreePath path = new TreePath(current.getPath());
+	setImmediate(true);
+	setSelectionPath(path);
+	setImmediate(false);
+    }
+
+    public void setTextNonSelectionColor(Color color) {
+	foreground_color = color;
+	if(isEnabled()) {
+	    ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(color);
+	}
+	else {
+	    ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(Color.black);
+	}
+	repaint();
+    }
+
+    public void setTextSelectionColor(Color color) {
+	((DefaultTreeCellRenderer)cellRenderer).setTextSelectionColor(color);
+	repaint();
+    }
+
+    public void valueChanged(TreeSelectionEvent event) {
+	if(group == null) {
+	    ///ystem.err.println("Oh my god, this tree has no group: " + this);
+	}
+	else {
+	    group.grabFocus(this);
+	}
+    }
+
+    /** returns false for dummy nodes (ones without files), and system root
+     * nodes */
+    private boolean isValidDrag() {
+	//because you cant select nodes that are children of another selection, and we use a contiguous selection model, we just test the first selection path
+	TreePath node_path = getSelectionPath();
+	FileNode node = (FileNode) node_path.getLastPathComponent();
+
+	if (node.getFile() == null) {
+	    return false;
+	}
+	if (node.isFileSystemRoot()) {
+	    return false;
+	}
+	// We also don't allow the user to select files that reside within the currently loaded collection
+	TreePath[] paths = getSelectionPaths();
+	for(int i = 0; i < paths.length; i++) {
+	    FileNode child = (FileNode) paths[i].getLastPathComponent();
+	    if (child.isInLoadedCollection()) {
+		return false;
+	    }
+	}
+	return true;
+    }
+
+    private boolean isValidDropOntoFile(TreePath target_path) {
+	boolean valid = false;
+	if(target_path != null) {
+	    FileNode target_node = (FileNode) target_path.getLastPathComponent();
+	    if (target_node.isLeaf()) {
+		// check the parent node for being a valid drop
+		return isValidDrop(target_path.getParentPath());
+	    }
+	}
+	return false;
+    }
+
+		
+    private boolean isValidDrop(TreePath target_path) {
+	boolean valid = false;
+	if(target_path == null) {
+	    previous_path = null;
+	} 
+	else {
+	    FileNode target_node = (FileNode) target_path.getLastPathComponent();
+	    // We can only continue testing if the node is a folder.
+	    if(!target_node.isLeaf()) {
+		// Now we check if the node is readonly.
+		if(!target_node.isReadOnly()) {
+		    // Finally we check the target path against the paths in the selection to ensure we are not adding to our own ancestors!
+		    TreePath[] selection = group.getSelection();
+		    boolean failed = false;
+		    for(int i = 0; !failed && selection != null && i < selection.length; i++) {
+			failed = selection[i].isDescendant(target_path);
+		    }
+		    // Having finally completed all the tests, we can highlight the drop target.
+		    if(!failed) {
+			valid = true;
+		    }
+		    else {
+			///ystem.err.println("Invalid. Target is descendant of itself.");
+		    }
+		}
+		else {
+		    ///ystem.err.println("Read only.");
+		}
+	    }
+	    else {
+		///ystem.err.println("Leaf node. Children not allowed.");
+	    }
+	    previous_path = target_path;
+	}
+	return valid;
+    }
+
+
+    /** This class listens for certain key presses, such as [Enter] or [Delete], and responds appropriately. */
+    private class DragTreeKeyListener
+	extends KeyAdapter
+    {
+	private boolean vk_left_pressed = false;
+	/** Called whenever a key that was pressed is released, it is this action that will cause the desired effects (this allows for the key event itself to be processed prior to this listener dealing with it). */
+	public void keyReleased(KeyEvent event) {
+	    ///ystem.err.println("Key Release detected. " + event.getKeyCode());
+	    if(event.getKeyCode() == KeyEvent.VK_DELETE) {
+		// Get the selected files from the tree and removal them using the default dnd removal method.
+		// Find the active tree (you've made selections in).
+		DragTree tree = (DragTree) group.getActive();
+		// Fudge things a bit
+		group.setSource(tree);
+		// Determine the selection.
+		TreePath paths[] = tree.getSelectionPaths();
+		if(paths != null) {
+		    FileNode[] source_nodes = new FileNode[paths.length];
+		    for(int i = 0; i < source_nodes.length; i++) {
+			source_nodes[i] = (FileNode) paths[i].getLastPathComponent();
+		    }
+		    Gatherer.f_man.action(tree, source_nodes, Gatherer.recycle_bin, null);
+		    source_nodes = null;
+		}
+	    }
+	    else if(event.getKeyCode() == KeyEvent.VK_ENTER) {
+		// Get the first selected file.
+		DragTree tree = (DragTree)event.getSource();
+		TreePath path = tree.getSelectionPath();
+		if(path != null) {
+		    File file = ((FileNode)path.getLastPathComponent()).getFile();
+		    if (file != null && file.isFile()) {
+			Gatherer.f_man.openFileInExternalApplication(file);
+		    }
+		    else {
+			if(!tree.isExpanded(path)) {
+			    tree.expandPath(path);
+			}
+			else {
+			    tree.collapsePath(path);
+			}
+		    }
+		}
+	    } else if (event.getKeyCode() == KeyEvent.VK_UP || event.getKeyCode() == KeyEvent.VK_DOWN) {
+		DragTree tree = (DragTree)event.getSource();
+		// we need to manually do the up and down selections
+		boolean up = (event.getKeyCode() == KeyEvent.VK_UP);
+		int current_row = tree.getLeadSelectionRow();
+		tree.setImmediate(true);
+		if (up) {
+		    if (current_row > 0) {
+			tree.clearSelection();
+			tree.setSelectionRow(current_row-1);
+		    }
+		} else {
+		    if (current_row < tree.getRowCount()-1){
+			tree.clearSelection();
+			tree.setSelectionRow(current_row+1);
+		    }
+		}
+		tree.setImmediate(false);
+	    } else if (event.getKeyCode() == KeyEvent.VK_LEFT) {
+		// left key on a file shifts the selection to the parent folder
+		DragTree tree = (DragTree)event.getSource();
+		TreePath path = tree.getLeadSelectionPath();
+		if(path != null) {
+		    File file = ((FileNode)path.getLastPathComponent()).getFile();
+		    if(file != null) {
+			if (file.isFile() || vk_left_pressed) {
+			    vk_left_pressed = false;
+			    TreePath parent_path = path.getParentPath();
+			    if (parent_path != null && parent_path.getParentPath() != null) {
+				// if this file is in the top level folder, don't move the focus 
+				tree.setImmediate(true);
+				tree.clearSelection();
+				tree.setSelectionPath(parent_path);
+				tree.setImmediate(false);
+			    }
+			}
+		    }
+		}
+	    }
+	}
+	
+	// we need to watch for left clicks on an unopened folder - should shift the focus to teh parent folder. But because there is some other mysterious key listener that does opening and closing folders on right and left clicks, we must detect the situation before the other handler has done its job, and process it after. 
+	public void keyPressed(KeyEvent event) {
+	    if (event.getKeyCode() == KeyEvent.VK_LEFT) {
+		// left key on  closed directory shifts the selection to the parent folder
+		DragTree tree = (DragTree)event.getSource();
+		TreePath path = tree.getLeadSelectionPath();
+		if(path == null) return; 
+		File file = ((FileNode)path.getLastPathComponent()).getFile();
+		if(file == null) return;
+		
+		if (file.isDirectory() && tree.isCollapsed(path)) {
+		    vk_left_pressed = true;
+		}
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/tree/DragTreeCellRenderer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/tree/DragTreeCellRenderer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/gui/tree/DragTreeCellRenderer.java	(revision 31635)
@@ -0,0 +1,89 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.gui.tree;
+
+import java.awt.*;
+import javax.swing.*;
+import javax.swing.tree.DefaultTreeCellRenderer;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.file.FileNode;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+
+public class DragTreeCellRenderer
+    extends DefaultTreeCellRenderer
+{
+    private Color selection_background;
+    private Color selection_foreground;
+
+    public DragTreeCellRenderer() {
+	super();
+	// Have to wait until this image is loaded
+	selection_background = getBackgroundSelectionColor();
+	selection_foreground = getTextSelectionColor();
+    }
+
+    public void gainFocus() {
+	setBackgroundSelectionColor(selection_background);
+	setTextSelectionColor(selection_foreground);
+    }
+
+    /** Configures the renderer based on the passed in components. */
+    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus)
+    {
+	// Rendering FileNodes
+	if (value instanceof FileNode) {
+	    FileNode node = (FileNode) value;
+	    String node_text = node.toString();
+
+	    // Add file sizes if necessary
+	    if (Configuration.get("general.show_file_size", Configuration.COLLECTION_SPECIFIC) && node.getFile() != null && !node.getAllowsChildren()) {
+		node_text = node_text + StaticStrings.SPACE_CHARACTER + StaticStrings.LBRACKET_CHARACTER + Utility.formatFileLength(node.getFile().length()) + StaticStrings.RBRACKET_CHARACTER;
+	    }
+
+	    return (JLabel) super.getTreeCellRendererComponent(tree, node_text, sel, expanded, leaf, row, hasFocus);
+	}
+
+	// Use default renderer for anything else
+	return (JLabel) super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
+    }
+
+    public void loseFocus() {
+	setBackgroundSelectionColor(Color.lightGray);
+	setTextSelectionColor(Color.black);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocGAFile.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocGAFile.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocGAFile.java	(revision 31635)
@@ -0,0 +1,45 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+import java.util.*;
+import java.net.URLDecoder;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.util.Utility;
+
+
+/** This class represents one doc.xml file */
+
+public class DocGAFile extends DocXMLFile
+{
+    public DocGAFile(String doc_xml_file_path)
+    {
+	super(doc_xml_file_path, "Description", "Metadata"); // metadataWrap and metadataItem
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocMetsXMLFile.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocMetsXMLFile.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocMetsXMLFile.java	(revision 31635)
@@ -0,0 +1,44 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+import java.util.*;
+import java.net.URLDecoder;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.util.Utility;
+
+
+/** This class represents one docmets.xml file */
+public class DocMetsXMLFile extends DocXMLFile
+{
+    public DocMetsXMLFile(String doc_xml_file_path)
+    {
+	super(doc_xml_file_path, "mets:xmlData", "ex:metadata"); // metadataWrap, metadataItem
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocXMLFile.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocXMLFile.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocXMLFile.java	(revision 31635)
@@ -0,0 +1,541 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+import java.util.*;
+import java.net.URLDecoder;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.util.Utility;
+
+
+/** This class represents one doc.xml file */
+
+public abstract class DocXMLFile extends File
+{
+    protected HashMap source_file_name_to_description_elements_mapping = new HashMap();
+
+    protected final String MetadataWrap;
+    protected final String MetadataItem;
+
+    public DocXMLFile(String doc_xml_file_path, String metaWrap, String metaItem)
+    {
+	super(doc_xml_file_path);
+	this.MetadataWrap = metaWrap;
+	this.MetadataItem = metaItem;
+    }
+
+
+    public ArrayList getMetadataExtractedFromFile(File file)
+    {
+	// Build up a list of metadata extracted from this file
+	ArrayList metadata_values = new ArrayList();
+
+	String file_relative_path = file.getAbsolutePath();
+	int import_index = file_relative_path.indexOf("import");
+	if (import_index != -1) {
+	    file_relative_path = file_relative_path.substring(import_index + "import".length() + 1);
+	}
+
+	// Check whether this file (i.e. doc.xml or docmets.xml on inheritance) file contains extracted metadata for the specified file
+	ArrayList description_elements_list = (ArrayList) source_file_name_to_description_elements_mapping.get(file_relative_path);
+	if (description_elements_list == null) {
+	    // ...it doesn't
+	    return metadata_values;
+	}
+
+ 	MetadataSet extracted_metadata_set = MetadataSetManager.getMetadataSet(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE);
+
+	// Parse the file
+	DebugStream.println("Applicable file: " + this);
+	try {
+	    BufferedReader buffered_reader = new BufferedReader(new InputStreamReader(new FileInputStream(this), "UTF-8"));
+
+	    int description_element_num = 0;
+	    int next_description_element_start = ((Integer) description_elements_list.get(description_element_num)).intValue();
+	    boolean in_relevant_description_element = false;
+
+	    String line = null;
+	    for (int line_num = 0; (line = buffered_reader.readLine()) != null; line_num++) {
+		// Check if this line contains the start of a relevant "Description" element
+		// (mets:xmlData in METS parlance, Description in GreenstoneArchive format) 
+		if (line_num == next_description_element_start) {
+		    in_relevant_description_element = true;
+		    continue;
+		}
+
+		// If we're not in a relevant Description element we don't care about anything
+		if (in_relevant_description_element == false) {
+		    continue;
+		}
+
+		// Check if this line contains the end of the relevant Description element
+		if (line.indexOf("</"+MetadataWrap+">") != -1) {
+		    description_element_num++;
+		    if (description_element_num == description_elements_list.size()) {
+			break;
+		    }
+
+		    next_description_element_start = ((Integer) description_elements_list.get(description_element_num)).intValue();
+		    in_relevant_description_element = false;
+		    continue;
+		}
+
+		// If this line doesn't contain a complete Metadata element, we're not interested
+		if (line.indexOf("<"+MetadataItem+" ") == -1 || line.indexOf("</"+MetadataItem+">") == -1) {
+		    continue;
+		}
+
+		// Extract the metadata element name
+		int name_index = line.indexOf(" name=\"") + " name=\"".length();
+		String metadata_element_name_full = line.substring(name_index, line.indexOf("\"", name_index));
+
+		// If the metadata has a namespace it isn't extracted metadata, so we're not interested
+		// Actually, if it is ex. then we are interested 
+		String metadata_set_namespace = MetadataTools.getMetadataSetNamespace(metadata_element_name_full);
+		
+		if (!metadata_set_namespace.equals("") && !metadata_set_namespace.equals("ex")) {
+		    continue;
+		}
+
+		// Extracted metadata!
+		// do it like this just in case we have ex.
+		String metadata_element_name = MetadataTools.getMetadataElementName(metadata_element_name_full);
+
+		// We completely ignore bibliographic data
+		if (metadata_element_name.equals("SourceSegment")) {
+		    buffered_reader.close();
+		    return new ArrayList();
+		}
+
+		// Ignore metadata starting with gsdl (used to be lowercase metadata and /metadata)
+		if (metadata_element_name.startsWith("gsdl")) {
+		    continue;
+		}
+
+		MetadataElement metadata_element = extracted_metadata_set.getMetadataElementWithName(metadata_element_name);
+
+		// Value trees are not stored for extracted metadata, so create a new value tree node now
+		int value_index = line.indexOf(">", name_index) + ">".length();
+		String metadata_element_value = line.substring(value_index, line.lastIndexOf("</"+MetadataItem+">"));
+
+		metadata_element.addMetadataValue(metadata_element_value);
+		MetadataValueTreeNode metadata_value_tree_node = metadata_element.getMetadataValueTreeNode(metadata_element_value);
+
+		// Add the new metadata value to the list
+		MetadataValue metadata_value = new MetadataValue(metadata_element, metadata_value_tree_node);
+		metadata_values.add(metadata_value);
+	    }
+
+	    buffered_reader.close();
+	}
+	catch (FileNotFoundException exception) {
+ 	    DebugStream.printStackTrace(exception);
+ 	}
+	catch (IOException exception) {
+ 	    DebugStream.printStackTrace(exception);
+ 	}
+
+	return metadata_values;
+    }
+
+
+
+
+    /**
+     * Every file must be skimmed when a collection is opened, for two reasons:
+     *   - To build a mapping from source file to its corresponding doc.xml file
+     *   - To get a complete list of all extracted metadata elements
+     */
+    public void skimFile()
+    {
+	MetadataSet extracted_metadata_set = MetadataSetManager.getMetadataSet(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE);
+
+	// Skim the file as quickly as possible (don't parse as XML), looking at the Metadata elements
+	DebugStream.println("Skimming " + this + "...");
+	try {
+	    BufferedReader buffered_reader = new BufferedReader(new InputStreamReader(new FileInputStream(this), "UTF-8"));
+	    int description_element_start = -1;
+
+	    String line = null;
+	    for (int line_num = 0; (line = buffered_reader.readLine()) != null; line_num++) {
+		// This line contains the start of a "MetadataWrap" element 
+		// (mets:xmlData in METS parlance, Description in GreenstoneArchive format) 
+		if (line.indexOf("<"+MetadataWrap+">") != -1) {
+		    if (description_element_start != -1) {
+			System.err.println("Parse error: previous " + MetadataWrap + " element unfinished!");
+		    }
+		    description_element_start = line_num;
+		    continue;
+		}
+
+		// This line contains the end of a "MetadataWrap" element
+		if (line.indexOf("</"+MetadataWrap+">") != -1) {
+		    if (description_element_start == -1) {
+			System.err.println("Parse error: "+MetadataWrap+" element unstarted!");
+		    }
+		    description_element_start = -1;
+		    continue;
+		}
+
+		// If we're not in a"MetadataWrap" element there shouldn't be any Metadata elements
+		if (description_element_start == -1) {
+		    continue;
+		}
+
+		// This line doesn't contain a Metadata element, so we're not interested
+		if (line.indexOf("<"+MetadataItem+" ") == -1) {
+		    DebugStream.println("Warning: "+MetadataWrap+" element line doesn't contain Metadata element.");
+		    continue;
+		}
+
+		// Extract the metadata element name
+		int name_index = line.indexOf(" name=\"") + " name=\"".length();
+		String metadata_element_name_full = line.substring(name_index, line.indexOf("\"", name_index));
+
+		// If the metadata has a namespace it isn't extracted metadata, so we're not interested
+		String metadata_set_namespace = MetadataTools.getMetadataSetNamespace(metadata_element_name_full);
+		if (!metadata_set_namespace.equals("") && !metadata_set_namespace.equals("ex")) {
+		    continue;
+		}
+
+		// Extracted metadata! May have ex. so make sure we remove that
+		String metadata_element_name = MetadataTools.getMetadataElementName(metadata_element_name_full);
+		// Note which file this is for
+		if (metadata_element_name.equals("gsdlsourcefilename")) {
+		    // Extract the gsdlsourcefilename element value
+		    int value_index = line.indexOf(">", name_index) + ">".length();
+		    String gsdlsourcefilename_value = line.substring(value_index, line.indexOf("<", value_index));
+
+		    // We're only interested in the path relative to the import folder
+		    int import_index = gsdlsourcefilename_value.indexOf("import");
+		    if (import_index != -1) {
+			gsdlsourcefilename_value = gsdlsourcefilename_value.substring(import_index + "import".length());
+
+			boolean is_unix_path = gsdlsourcefilename_value.startsWith("/");
+			gsdlsourcefilename_value = gsdlsourcefilename_value.substring(1);
+
+			// URL decode gsdlsourcefilename. Need to set the decoder to use the default system encoding
+			// This is stored in the System's file.encoding property.
+			gsdlsourcefilename_value = URLDecoder.decode(gsdlsourcefilename_value, System.getProperty("file.encoding")); 
+
+			// Make sure the path matches the OS that is running
+			if (is_unix_path && Utility.isWindows()) {
+			    // Convert path from Unix to Windows
+			    gsdlsourcefilename_value = gsdlsourcefilename_value.replaceAll("\\/", "\\\\");
+			}
+			else if (!is_unix_path && !Utility.isWindows()) {
+			    // Convert path from Windows to Unix
+			    gsdlsourcefilename_value = gsdlsourcefilename_value.replaceAll("\\\\", "/");
+			}
+
+			// Remember this for quick access later
+			if (source_file_name_to_description_elements_mapping.get(gsdlsourcefilename_value) == null) {
+			    source_file_name_to_description_elements_mapping.put(gsdlsourcefilename_value, new ArrayList());
+			}
+
+			((ArrayList) source_file_name_to_description_elements_mapping.get(gsdlsourcefilename_value)).add(new Integer(description_element_start));
+		    }
+
+		    // Warn about an odd gsdlsourcefilename, except if it is the Greenstone "tmp" directory or
+		    // (as in the case of using FLI) if it is the etc/collect.cfg or etc/collectionConfig.xml file
+		    // which are the gsdlsourcefilenames for the fedora digital object representing a collection.
+		    // This (tmp dir) is true when the source files come from a zip file processed by ZIPPlug, for example
+		    else if (gsdlsourcefilename_value.indexOf("tmp") == -1 
+			     && !gsdlsourcefilename_value.endsWith("collect.cfg")
+			     && !gsdlsourcefilename_value.endsWith("collectionConfig.xml")) {
+			// We don't really know what is going on...
+			System.err.println("Warning: Could not understand gsdlsourcefilename " + gsdlsourcefilename_value);
+		    }
+		}
+
+		// Ignore metadata starting with gsdl (used to be lowercase metadata and /metadata)
+		if (metadata_element_name.startsWith("gsdl")) {
+		    continue;
+		}
+
+		MetadataElement metadata_element = extracted_metadata_set.getMetadataElementWithName(metadata_element_name);
+		if (metadata_element == null) {
+		    // This element isn't defined in ex.mds, so create it for this session
+		    DebugStream.println("Extracted metadata element not defined: " + metadata_element_name);
+		    extracted_metadata_set.addMetadataElementForThisSession(metadata_element_name);
+		}
+	    }
+
+	    buffered_reader.close();
+	}
+	catch (FileNotFoundException exception) {
+ 	    DebugStream.printStackTrace(exception);
+ 	}
+	catch (IOException exception) {
+ 	    DebugStream.printStackTrace(exception);
+ 	}
+    }
+
+
+    /*
+    public ArrayList getMetadataExtractedFromFile(File file)
+    {
+	// Build up a list of metadata extracted from this file
+	ArrayList metadata_values = new ArrayList();
+
+	String file_relative_path = file.getAbsolutePath();
+	int import_index = file_relative_path.indexOf("import");
+	if (import_index != -1) {
+	    file_relative_path = file_relative_path.substring(import_index + "import".length() + 1);
+	}
+
+	// Check whether this doc.xml file contains extracted metadata for the specified file
+	ArrayList description_elements_list = (ArrayList) source_file_name_to_description_elements_mapping.get(file_relative_path);
+	if (description_elements_list == null) {
+	    // ...it doesn't
+	    return metadata_values;
+	}
+
+ 	MetadataSet extracted_metadata_set = MetadataSetManager.getMetadataSet(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE);
+
+	// Parse the doc.xml file
+	DebugStream.println("Applicable doc.xml file: " + this);
+	try {
+	    BufferedReader buffered_reader = new BufferedReader(new FileReader(this));
+
+	    int description_element_num = 0;
+	    int next_description_element_start = ((Integer) description_elements_list.get(description_element_num)).intValue();
+	    boolean in_relevant_description_element = false;
+
+	    String line = null;
+	    for (int line_num = 0; (line = buffered_reader.readLine()) != null; line_num++) {
+		// Check if this line contains the start of a relevant Description element
+		if (line_num == next_description_element_start) {
+		    in_relevant_description_element = true;
+		    continue;
+		}
+
+		// If we're not in a relevant Description element we don't care about anything
+		if (in_relevant_description_element == false) {
+		    continue;
+		}
+
+		// Check if this line contains the end of the relevant Description element
+		if (line.indexOf("</Description>") != -1) {
+		    description_element_num++;
+		    if (description_element_num == description_elements_list.size()) {
+			break;
+		    }
+
+		    next_description_element_start = ((Integer) description_elements_list.get(description_element_num)).intValue();
+		    in_relevant_description_element = false;
+		    continue;
+		}
+
+		// If this line doesn't contain a complete Metadata element, we're not interested
+		if (line.indexOf("<Metadata ") == -1 || line.indexOf("</Metadata>") == -1) {
+		    continue;
+		}
+
+		// Extract the metadata element name
+		int name_index = line.indexOf(" name=\"") + " name=\"".length();
+		String metadata_element_name_full = line.substring(name_index, line.indexOf("\"", name_index));
+
+		// If the metadata has a namespace it isn't extracted metadata, so we're not interested
+		String metadata_set_namespace = MetadataTools.getMetadataSetNamespace(metadata_element_name_full);
+		if (!metadata_set_namespace.equals("")) {
+		    continue;
+		}
+
+		// Extracted metadata!
+		String metadata_element_name = metadata_element_name_full;
+
+		// We completely ignore bibliographic data
+		if (metadata_element_name.equals("SourceSegment")) {
+		    buffered_reader.close();
+		    return new ArrayList();
+		}
+
+		// Ignore metadata starting with gsdl (used to be lowercase metadata and /metadata)
+		if (metadata_element_name.startsWith("gsdl")) {
+		    continue;
+		}
+
+		MetadataElement metadata_element = extracted_metadata_set.getMetadataElementWithName(metadata_element_name);
+
+		// Value trees are not stored for extracted metadata, so create a new value tree node now
+		int value_index = line.indexOf(">", name_index) + ">".length();
+		String metadata_element_value = line.substring(value_index, line.lastIndexOf("</Metadata>"));
+
+		metadata_element.addMetadataValue(metadata_element_value);
+		MetadataValueTreeNode metadata_value_tree_node = metadata_element.getMetadataValueTreeNode(metadata_element_value);
+
+		// Add the new metadata value to the list
+		MetadataValue metadata_value = new MetadataValue(metadata_element, metadata_value_tree_node);
+		metadata_values.add(metadata_value);
+	    }
+
+	    buffered_reader.close();
+	}
+	catch (FileNotFoundException exception) {
+ 	    DebugStream.printStackTrace(exception);
+ 	}
+	catch (IOException exception) {
+ 	    DebugStream.printStackTrace(exception);
+ 	}
+
+	return metadata_values;
+    }
+
+    */
+
+    /**
+     * Every doc.xml file must be skimmed when a collection is opened, for two reasons:
+     *   - To build a mapping from source file to its corresponding doc.xml file
+     *   - To get a complete list of all extracted metadata elements
+     */
+    /*
+    public void skimFile()
+    {
+	MetadataSet extracted_metadata_set = MetadataSetManager.getMetadataSet(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE);
+
+	// Skim the doc.xml file as quickly as possible (don't parse as XML), looking at the Metadata elements
+	DebugStream.println("Skimming " + this + "...");
+	try {
+	    BufferedReader buffered_reader = new BufferedReader(new FileReader(this));
+	    int description_element_start = -1;
+
+	    String line = null;
+	    for (int line_num = 0; (line = buffered_reader.readLine()) != null; line_num++) {
+		// This line contains the start of a Description element
+		if (line.indexOf("<Description>") != -1) {
+		    if (description_element_start != -1) {
+			System.err.println("Parse error: previous Description element unfinished!");
+		    }
+		    description_element_start = line_num;
+		    continue;
+		}
+
+		// This line contains the end of a Description element
+		if (line.indexOf("</Description>") != -1) {
+		    if (description_element_start == -1) {
+			System.err.println("Parse error: Description element unstarted!");
+		    }
+		    description_element_start = -1;
+		    continue;
+		}
+
+		// If we're not in a Description element there shouldn't be any Metadata elements
+		if (description_element_start == -1) {
+		    continue;
+		}
+
+		// This line doesn't contain a Metadata element, so we're not interested
+		if (line.indexOf("<Metadata ") == -1) {
+		    DebugStream.println("Warning: Description element line doesn't contain Metadata element.");
+		    continue;
+		}
+
+		// Extract the metadata element name
+		int name_index = line.indexOf(" name=\"") + " name=\"".length();
+		String metadata_element_name_full = line.substring(name_index, line.indexOf("\"", name_index));
+
+		// If the metadata has a namespace it isn't extracted metadata, so we're not interested
+		String metadata_set_namespace = MetadataTools.getMetadataSetNamespace(metadata_element_name_full);
+		if (!metadata_set_namespace.equals("")) {
+		    continue;
+		}
+
+		// Extracted metadata!
+		String metadata_element_name = metadata_element_name_full;
+
+		// Note which file this doc.xml is for
+		if (metadata_element_name.equals("gsdlsourcefilename")) {
+		    // Extract the gsdlsourcefilename element value
+		    int value_index = line.indexOf(">", name_index) + ">".length();
+		    String gsdlsourcefilename_value = line.substring(value_index, line.indexOf("<", value_index));
+
+		    // We're only interested in the path relative to the import folder
+		    int import_index = gsdlsourcefilename_value.indexOf("import");
+		    if (import_index != -1) {
+			gsdlsourcefilename_value = gsdlsourcefilename_value.substring(import_index + "import".length());
+
+			boolean is_unix_path = gsdlsourcefilename_value.startsWith("/");
+			gsdlsourcefilename_value = gsdlsourcefilename_value.substring(1);
+
+			// URL decode gsdlsourcefilename. Need to set the decoder to use the default system encoding
+			// This is stored in the System's file.encoding property.
+			gsdlsourcefilename_value = URLDecoder.decode(gsdlsourcefilename_value, System.getProperty("file.encoding")); 
+
+			// Make sure the path matches the OS that is running
+			if (is_unix_path && Utility.isWindows()) {
+			    // Convert path from Unix to Windows
+			    gsdlsourcefilename_value = gsdlsourcefilename_value.replaceAll("\\/", "\\\\");
+			}
+			else if (!is_unix_path && !Utility.isWindows()) {
+			    // Convert path from Windows to Unix
+			    gsdlsourcefilename_value = gsdlsourcefilename_value.replaceAll("\\\\", "/");
+			}
+
+			// Remember this for quick access later
+			if (source_file_name_to_description_elements_mapping.get(gsdlsourcefilename_value) == null) {
+			    source_file_name_to_description_elements_mapping.put(gsdlsourcefilename_value, new ArrayList());
+			}
+
+			((ArrayList) source_file_name_to_description_elements_mapping.get(gsdlsourcefilename_value)).add(new Integer(description_element_start));
+		    }
+
+		    // Warn about an odd gsdlsourcefilename, except if it is the Greenstone "tmp" directory
+		    // This is true when the source files come from a zip file processed by ZIPPlug, for example
+		    else if (gsdlsourcefilename_value.indexOf("tmp") == -1) {
+			// We don't really know what is going on...
+			System.err.println("Warning: Could not understand gsdlsourcefilename " + gsdlsourcefilename_value);
+		    }
+		}
+
+		// Ignore metadata starting with gsdl (used to be lowercase metadata and /metadata)
+		if (metadata_element_name.startsWith("gsdl")) {
+		    continue;
+		}
+
+		MetadataElement metadata_element = extracted_metadata_set.getMetadataElementWithName(metadata_element_name);
+		if (metadata_element == null) {
+		    // This element isn't defined in ex.mds, so create it for this session
+		    DebugStream.println("Extracted metadata element not defined: " + metadata_element_name);
+		    extracted_metadata_set.addMetadataElementForThisSession(metadata_element_name);
+		}
+	    }
+
+	    buffered_reader.close();
+	}
+	catch (FileNotFoundException exception) {
+ 	    DebugStream.printStackTrace(exception);
+ 	}
+	catch (IOException exception) {
+ 	    DebugStream.printStackTrace(exception);
+ 	}
+    }
+    */
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocXMLFileManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocXMLFileManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/DocXMLFileManager.java	(revision 31635)
@@ -0,0 +1,105 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.DebugStream;
+
+
+/** This class is a static class that manages the doc.xml files */
+public class DocXMLFileManager
+{
+    static private ArrayList doc_xml_files = new ArrayList();
+
+
+    static public void clearDocXMLFiles()
+    {
+	doc_xml_files.clear();
+    }
+
+
+    static public ArrayList getMetadataExtractedFromFile(File file)
+    {
+	// Build up a list of metadata values extracted from this file
+	ArrayList metadata_values = new ArrayList();
+
+	// Look at each loaded doc.xml file to see if any have extracted metadata for this file
+	for (int i = 0; i < doc_xml_files.size(); i++) {
+	    DocXMLFile doc_xml_file = (DocXMLFile) doc_xml_files.get(i);
+	    metadata_values.addAll(doc_xml_file.getMetadataExtractedFromFile(file));
+	}
+
+	return metadata_values;
+    }
+
+
+    static public void loadDocXMLFiles(File directory, String filename_match)
+    {
+	// Make sure the directory (archives) exists
+	if (directory.exists() == false) {
+	    return;
+	}
+
+	// Look recursively at each subfile of the directory for doc.xml files
+	File[] directory_files = directory.listFiles();
+	for (int i = 0; i < directory_files.length; i++) {
+	    File child_file = directory_files[i];
+	    if (child_file.isDirectory()) {
+		loadDocXMLFiles(child_file,filename_match); 
+	    }
+	    else if (child_file.getName().equals(filename_match)) {
+		// e.g. doc.xml (for regular Greenstone, docmets.xml for Fedora)
+
+		loadDocXMLFile(child_file,filename_match);
+	    }
+	}
+    }
+
+
+    static private void loadDocXMLFile(File doc_xml_file_file,String filename_match)
+    {
+	String file = doc_xml_file_file.getAbsolutePath();
+
+	// Need to do typecasts in the following to keep Java 1.4 happy
+	DocXMLFile doc_xml_file 
+	    = (filename_match.equals("docmets.xml")) 
+	    ? (DocXMLFile) new DocMetsXMLFile(file) 
+	    : (DocXMLFile) new DocGAFile(file);
+
+	try {	    
+	    doc_xml_file.skimFile();
+	    doc_xml_files.add(doc_xml_file);
+	}
+	catch (Exception exception) {
+	    // We catch any exceptions here so errors in doc.xml files don't stop the collection from loading
+	    System.err.println("Error: Could not skim doc.xml file " + doc_xml_file.getAbsolutePath());
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/FilenameEncoding.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/FilenameEncoding.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/FilenameEncoding.java	(revision 31635)
@@ -0,0 +1,439 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2010 Greenstone Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+ 
+package org.greenstone.gatherer.metadata;
+
+import java.io.File;
+import java.net.*;
+import java.nio.charset.*;
+import java.util.*;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.DebugStream;
+
+/** Static access class that contains many of the methods used to work with filename encodings. 
+* Works closely with classes FileNode, CollectionTreeNode, MetadataXMLFile, MetadataXMLFileManager
+* to maintain a map of URLEncodedFilenames to their filename encodings.
+* The process of filename encoding further affects the CollectionManager which refreshes its CollectionTree, 
+* FileManager (move, delete, rename actions), MetadataValueTableModel, EnrichPane. */
+
+public class FilenameEncoding {
+	/** Display of filenames in the trees are in URL encoding, if debugging */
+	public static boolean DEBUGGING = false;
+
+	/** Set to false by Gatherer if the locale is UTF-8, as Java's handling is 
+	* such that non-UTF8 filename encodings on a UTF-8 locale are destructively 
+	* converted so that the bytecodes in the filename are not preserved. */
+    public static boolean MULTIPLE_FILENAME_ENCODINGS_SUPPORTED = false;
+
+	/** Also set by Gatherer. 
+	* If the OS supports multiple filename encodings, we will be working with URL strings
+	* and the applicable separators are always the forward slash ("/") not File.separator. 
+	* If  multiple filename encodings are not supported, we're dealing with File.separator. */
+    public static String URL_FILE_SEPARATOR = File.separator;
+    
+
+	/** gs.filenameEncoding is a special sort of metadata that is not merely to be stored along 
+	 * with a file, but is to be applied in real-time on the file's name in the CollectionTree 
+	 * display. Since FileNodes are constantly destroyed and reconstructed by that Tree when 
+	 * its nodes are expanded and contracted, storing the filename encodings of each file along
+	 * with the file in a FileNode doesn't help because it doesn't last. Instead of rediscovering
+	 * the encoding at every stage by querying the metadataXML file, we store the encodings for
+	 * fast access: in a map of (URLEncodedFilePath, filename-encoding) pairs. 
+	 * The current design of the map is to only store any active filename metadata assigned
+	 * directly at that file/folder's level, and if there is none discovered at that level, then
+	 * storing the empty string for it. Therefore, if the hashmap contains no entry for 
+	 * a file, it means this still needs to be retrieved. */
+    public static Map map = new HashMap();
+
+//*********************** BUSY REFRESHING / REQUIRING  REFRESH *********************
+
+    /** Set to true if filename encoding metadata was changed. Called by the enter keyPress 
+	* event in gui.EnrichPane and when the gs.FilenameEncoding field loses focus. */
+    private static boolean refreshRequired = false;
+
+    synchronized public static boolean isRefreshRequired() { 
+		return refreshRequired; 
+	}
+	
+    synchronized public static void setRefreshRequired(boolean state) { 
+		if(MULTIPLE_FILENAME_ENCODINGS_SUPPORTED) {
+			refreshRequired = state;
+		} else {
+			refreshRequired = false; 
+		}		
+	}
+
+//************************** MAP RETRIEVAL METHODS ******************************	
+
+	/** Returns the cumulative gs.filenameEncoding metadata 
+	* assigned to a file inside the collection. */    
+	public static String findFilenameEncoding(
+			File file, String urlEncodedFilePath, boolean bruteForceLookup) 
+	{
+		//if(bruteForceLookup) {
+		//	return findFilenameEncodingBruteForce(file, urlEncodedFilePath, bruteForceLookup);
+		//}		
+	
+		String encoding = "";
+		
+		// Check any assigned encoding at this level, starting with the map first
+		// and else retrieving the filename encoding from the metadata file
+		if(!map.containsKey(urlEncodedFilePath)) {
+		
+			// Check for filename encoding metadata *directly* associated with the file
+			// Now don't need to get any inherited encoding metadata here, because of 
+			// the way we're storing and retrieving encoding information from the map.			
+			ArrayList list = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(file, true); // true: gets gs.filenameEncoding only
+			if(!list.isEmpty()) {
+			    MetadataValue metavalue = (MetadataValue)list.get(0); // get(list.size()-1); 
+			    encoding = metavalue.getValue();			    
+			} // else no filename encoding set yet at this level
+		
+			// Now we've done a lookup at this level cache the result in the map,
+			// including empty strings, to indicate that we've done a full lookup
+			map.put(urlEncodedFilePath, encoding);
+		} 
+		else { // an entry exists in the map, get it from there
+			encoding = (String)map.get(urlEncodedFilePath);
+		}	
+		
+		// if no meta was specified at at the file level, look for any inherited metadata
+		if(encoding.equals("")) {
+			encoding = getInheritedFilenameEncoding(urlEncodedFilePath, file);
+		}
+		
+		//System.err.println("\n@@@@Looked for: " + urlEncodedFilePath + " | found: " + encoding);
+		return encoding; // found something in map, may still be "", but it's what was stored
+	}
+	
+	/** Checks the file-to-encoding map for all the superfolders of the given 
+	 * filename in sequence for an applicable encoding. Note that the file/folder
+	 * at the level of urlFoldername (and dir) has already been inspected. */
+    static public String getInheritedFilenameEncoding(String urlFoldername, File dir) 
+	{
+		String encoding = "";
+		boolean done = false;
+		
+		// don't want to search past import folder which is as 
+		// far as we need to go to determine inherited encodings
+		File importDir = new File(CollectionManager.getLoadedCollectionImportDirectoryPath());
+		if(dir.equals(importDir)) { // if the top-level dir was already checked, we're done
+			done = true;
+		}
+
+		// For directories, first remove trailing file separator in order to start checking from higher level folders
+		int lastIndex = urlFoldername.length()-1;
+		char urlFileSeparatorChar = URL_FILE_SEPARATOR.charAt(0);
+		if(urlFoldername.charAt(lastIndex) == urlFileSeparatorChar) { 
+		    urlFoldername = urlFoldername.substring(0, lastIndex);
+		}
+
+		while(!done) {
+			// get the folder that's one level up
+			dir = dir.getParentFile();		
+			
+		    int index = urlFoldername.lastIndexOf(URL_FILE_SEPARATOR);		
+		    if(index == -1) { // no more slashes
+				done = true;
+		    } else { 
+				urlFoldername = urlFoldername.substring(0, index);
+		    }
+		    
+			// now look in the map to see whether there's an encoding for this folder
+		    String folder = urlFoldername + URL_FILE_SEPARATOR;
+		    if(map.containsKey(folder)) {
+				encoding = (String)map.get(folder); // may be ""				
+		    } else { // no entry in map, so look in the metadata.xml at this folder level
+				ArrayList list = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(
+						dir, true); // true: gets gs.filenameEncoding only
+				if(!list.isEmpty()) {
+				    MetadataValue metavalue = (MetadataValue)list.get(0); // get(list.size()-1); 
+				    encoding = metavalue.getValue();			    
+				}
+				map.put(folder, encoding); // may be ""
+			}
+			
+			if(!encoding.equals("")){
+			    done = true;
+			} // else if "", loop to check next folder up
+			else if(dir.equals(importDir)) { // don't iterate past the import folder, which we've now checked
+				done = true;
+			}
+		}
+
+		return encoding;
+    }
+
+	/** Called by GUIManager when a collection is closed. This then empties the 
+	* file-to-encoding map which is applicable only on a per-collection basis */
+    static public void closeCollection() {	
+		//printFilenameMap("Closing collection. Clearing file-to-encoding map of entries:");
+		map.clear();
+    }
+
+	// Useful for debugging: prints contents of file-to-encoding map
+    static public void printFilenameMap(String heading) {
+		System.err.println("\n********************************************");
+		System.err.println(heading.toUpperCase());
+		Iterator entries = map.entrySet().iterator();
+		while(entries.hasNext()) {
+		    Map.Entry entry = (Map.Entry)entries.next();
+		    System.err.println("+ " + (String)entry.getKey() + ": " + (String)entry.getValue());
+		}
+		System.err.println("********************************************\n");
+    }
+	
+	// UNUSED at present. Brute force version of the findFilenameEncoding() method
+	// Doesn't use the map, but gets *all* the metadata assigned to a file/folder to 
+	// work out the encoding applicable to a file/folder.
+    public static String findFilenameEncodingBruteForce(File file, String urlEncodedFilename, 
+					     boolean bruteForceLookup) 
+    {
+		System.err.println("\n***** BRUTE FORCE getFilenameEncoding() called\n");
+	
+	
+		String encoding = "";
+
+		// Check for filename encoding metadata *directly* associated with the file
+		// Now don't need to get any inherited encoding metadata here, because of 
+		// the way we're storing and retrieving encoding information from the map.
+
+		ArrayList list = MetadataXMLFileManager.getMetadataAssignedToFile(file, true); // true: gets gs.filenameEncoding only
+		if(!list.isEmpty()) {	   
+		    // try to get the filename encoding meta that was assigned last to this 
+		    // file, even though it makes no sense to have multiple values for it
+		    MetadataValue metavalue = (MetadataValue)list.get(list.size()-1); 
+		    encoding = metavalue.getValue();
+		    
+		    if(encoding == null) { // unlikely ???
+				System.err.println("**** ERROR: encoding for " 
+						+ urlEncodedFilename + " is NULL!");    
+				encoding = "";
+		    }
+		} // else no filename encoding set yet, perhaps
+		//System.err.println("**** Found encoding for " + urlEncodedFilename + " " + encoding);
+		return encoding;
+    }
+
+//****************************** APPLYING ENCODINGS TO FILENAMES *****************************
+	
+    /** URL encoded version of the byte codes of the given file's name */
+    public static String calcURLEncodedFilePath(File file) {	
+		if(!MULTIPLE_FILENAME_ENCODINGS_SUPPORTED) {
+		    return file.getAbsolutePath();
+		}
+		else {
+		    String filename = fileToURLEncoding(file);
+		    return filename;
+		}
+    }
+
+    /** URL encoded version of the byte codes of this file's name */
+    public static String calcURLEncodedFileName(String urlfilepath) {
+		String filename = urlfilepath;
+		if(filename.endsWith(URL_FILE_SEPARATOR)) { // directory, remove trailing slash
+		    filename = filename.substring(0, filename.length() - 1);
+		}	    
+		
+		// remove the directory prefix (if any) to get the filename
+		int index = filename.lastIndexOf(URL_FILE_SEPARATOR);
+		if(index != -1) {
+		    filename = filename.substring(index+1); // skip separator
+		}
+		
+		return filename;    
+    }
+
+	/** Given a string representing an alias to an official encoding (and unofficial ones 
+	* starting with "Latin-"), attempts to work out what the canonical encoding for that is. 
+	* If the given encoding is unrecognised, it is returned as is. */
+    public static String canonicalEncodingName(String encoding) {
+		String canonicalEncoding = encoding;
+		try {
+			// Latin-1 -> ISO-8859-1
+			String alias = canonicalEncoding.toLowerCase();
+			if(alias.startsWith("latin")){								
+				canonicalEncoding = "ISO-8859" + alias.substring("latin".length());
+			}
+
+			// canonical encoding for official aliases
+			canonicalEncoding = Charset.forName(canonicalEncoding).name();			
+			return canonicalEncoding;
+		} catch (Exception e) {
+			System.err.println("(Could not recognise encoding (alias): " 
+					+ encoding + ".)");
+		    return encoding; // no alias could be found, return the original parameter
+		}
+    }
+
+//************************* GETTING THE URL ENCODING OF FILENAMES *********************************
+	// Dr Bainbridge's methods
+	/* On Linux machines that are set to using an ISO-8859 (Latin) type encoding, 
+	* we can work with URL-encoded filenames in Java. Java works with whatever 
+	* encoding the filesystem uses. Unlike systems working with UTF-8, where Java 
+	* interprets filenames as UTF-8 (a destructive process since characters invalid
+	* for UTF-8 are replaced with the invalid character, which means the original
+	* character's byte codes can not be regained), working with an ISO-8859-1 
+	* system means the original byte codes of the characters are preserved, 
+	* regardless of whether the characters represent ISO-8859-1 or not. Such byte
+	* codes are converted by the following method to the correct URL versions of 
+	* the strings that the filenames represent (that is, the correct URL representations
+	* of the filenames in their original encodings). This is useful for interactions with
+	* Perl as Java and Perl can use URL-encoded filenames to talk about the same files
+	* on the file system, instead of having to work out what encoding they are in. */
+	
+    public static String fileToURLEncoding(File file) {
+		if(!MULTIPLE_FILENAME_ENCODINGS_SUPPORTED) {
+		    return file.getAbsolutePath();
+		}
+
+		String filename_url_encoded = "";
+		
+		// The following test for whether the file exists or not is a problem
+		// when a File object--whose actual file is in the process of being moved 
+		// and therefore temporarily does not 'exist' on the actual system--can't
+		// be URL encoded: the following would return "" when a file doesn't exist.
+		// So commenting out the test.
+		/* 
+		if(!file.getName().equals("recycle")) {
+		    if(!file.isFile() && !file.isDirectory()) {
+			System.err.println("*** ERROR. Java can't see file: " + file.getAbsolutePath());
+			return "";
+		    }
+		    
+		    if(!file.exists()) {
+			System.err.println("*** NOTE: File doesn't exist: " + file.getAbsolutePath());
+			return ""; //file.getName();
+		    }
+		}
+		*/
+
+		URI filename_uri = file.toURI();
+		try {
+		    // The trick:
+		    //  1. toASCIIString() will %xx encode values > 127
+		    //  2. Decode the result to "ISO-8859-1" 
+		    //  3. URL encode the bytes to string
+		    
+		    // Step 2 forces the string to be 8-bit values.  It
+		    // doesn't matter if the starting raw filename was *not*
+		    // in the ISO-8859-1 encoding, the effect is to ensure
+		    // we have an 8-bit byte string that (numerically)
+		    // captures the right value.  These numerical values are
+		    // then used to determine how to URL encode it
+		    
+		    String filename_ascii = filename_uri.toASCIIString();
+		    String filename_raw_bytes = URLDecoder.decode(filename_ascii,"ISO-8859-1");
+		    filename_url_encoded = iso_8859_1_filename_to_url_encoded(filename_raw_bytes);
+			
+		}
+		catch (Exception e) {
+		    e.printStackTrace();
+		    // Give up trying to convert
+		    filename_url_encoded = file.getAbsolutePath(); 
+		}
+		return filename_url_encoded;
+    }
+
+    
+    // For unicode codepoints see:
+    // http://unicode.org/Public/MAPPINGS/ISO8859/8859-1.TXT for ISO8859-1 (Latin-1)
+    // where 0xE2 maps to codepoint 0x00E2 and is defined as "Latin small letter a with circumflex"
+    // http://unicode.org/Public/MAPPINGS/ISO8859/8859-7.TXT for ISO8859-7 (Greek)
+    // where 0xE2 maps to codepoint 0x03B2 and is defined as "Greek small letter beta"    
+    public static String iso_8859_1_filename_to_url_encoded(String raw_bytes_filename) 
+		throws Exception
+    {
+		String urlEncoded = "";
+
+		try {
+		    // By this point we have a UTF-8 encoded string that captures
+		    // what the ISO-8859-1 (Latin-1) character is that corresponded to the
+		    // 8-bit numeric value for that character in the filename
+		    // on the file system
+		    
+		    // For example:
+		    //   File system char:             <lower-case beta char in Latin-7> = %E2
+		    //   Equivalent Latin 1 char:      <lower-case a with circumflex>    = %E2
+		    //   Mapped to UTF-8:              <lower-case a with circumflex>    = <C3><A2>
+
+		    // Our task is to take the string the contains <C3><A2> and ensure that
+		    // we "see" it as <E2>
+
+		    byte [] raw_bytes = raw_bytes_filename.getBytes("ISO-8859-1");
+		    String unicode_filename = new String(raw_bytes,"UTF-8");
+		    
+		    for(int i = 0; i < unicode_filename.length(); i++) {
+			char charVal = unicode_filename.charAt(i);
+			if ((int)charVal > 255) {
+				urlEncoded += String.format("&#x%02X;", (int)charVal);
+			}
+			else if((int)charVal > 127) {
+			    urlEncoded += String.format("%%%02X", (int)charVal);
+			} else {
+			    urlEncoded += String.format("%c", (char)charVal);
+			}
+		    }
+		}
+		catch (Exception e) {
+		    //e.printStackTrace();
+		    throw(e);
+		}
+
+		return urlEncoded;
+    }
+
+    // unused for now
+    public static String raw_filename_to_url_encoded(String fileName)
+		throws Exception
+    {
+		String urlEncoded = "";
+		try {
+		    byte[] bytes = fileName.getBytes();
+		    
+		    for(int i = 0; i < bytes.length; i++) {
+			// mask each byte (by applying & 0xFF) to make the signed
+			// byte (in the range -128 to 127) unsigned (in the range
+			// 0 to 255).
+
+			int byteVal = (int)(bytes[i] & 0xFF);
+			
+			if(byteVal > 127) {
+			    urlEncoded += String.format("%%%02X", (int)byteVal);
+			} else {
+			    urlEncoded += String.format("%c",(char)byteVal);
+			}
+		    }
+		}
+		catch (Exception e) {
+		    //e.printStackTrace();
+		    throw(e);
+		}
+
+		return urlEncoded;
+    }
+   
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataAuditTableModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataAuditTableModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataAuditTableModel.java	(revision 31635)
@@ -0,0 +1,240 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.table.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+
+
+public class MetadataAuditTableModel
+    extends AbstractTableModel
+{
+    /** It is not expected that this class will ever be serialized */
+    private static final long serialVersionUID = 0L;
+
+    /** The CollectionTreeNodes this model is built for */
+    private CollectionTreeNode[] file_nodes = null;
+    /** The list of MetadataElements in the table (columns) */
+    private ArrayList metadata_elements = new ArrayList();
+    /** The list of MetadataAuditTableEntries in the table (rows) */
+    private ArrayList metadata_audit_table_entries = new ArrayList();
+
+
+    /** Returns the number of columns in this table. */
+    public int getColumnCount()
+    {
+	return 1 + metadata_elements.size();
+    }
+
+
+    /** Returns the name of the specified column. */
+    public String getColumnName(int col)
+    {
+	// First column: filename
+	if (col == 0) {
+	    return Dictionary.get("AuditTable.File");
+	}
+
+	int metadata_element_index = col - 1;
+	return metadata_elements.get(metadata_element_index).toString();
+    }
+
+
+    /** Returns the values in a particular column. */
+    public ArrayList getColumnValues(int col)
+    {
+	// First column: filename
+	if (col == 0) {
+	    ArrayList column_values = new ArrayList();
+	    for (int i = 0; i < metadata_audit_table_entries.size(); i++) {
+		MetadataAuditTableEntry metadata_audit_table_entry = (MetadataAuditTableEntry) metadata_audit_table_entries.get(i);
+		column_values.add(metadata_audit_table_entry.getFilename());
+	    }
+
+	    return column_values;
+	}
+
+	// Check values are reasonable
+	int metadata_element_index = col - 1;
+	if (metadata_element_index < 0 || metadata_element_index >= metadata_elements.size()) {
+	    return null;
+	}
+
+	ArrayList column_values = new ArrayList();
+	for (int i = 0; i < metadata_audit_table_entries.size(); i++) {
+	    MetadataAuditTableEntry metadata_audit_table_entry = (MetadataAuditTableEntry) metadata_audit_table_entries.get(i);
+	    MetadataValueTreeNode[] metadata_value_tree_nodes = metadata_audit_table_entry.getMetadataValueTreeNodes(metadata_element_index);
+	    if (metadata_value_tree_nodes != null) {
+		for (int j = 0; j < metadata_value_tree_nodes.length; j++) {
+		    column_values.add(metadata_value_tree_nodes[j].toString());
+		}
+	    }
+	}
+
+	return column_values;
+    }
+
+
+    /** Returns the number of rows in this table. */
+    public int getRowCount()
+    {
+	return metadata_audit_table_entries.size();
+    }
+
+
+    /** Returns the cell value at a given row and column as an Object. */
+    public Object getValueAt(int row, int col)
+    {
+	// Check values are reasonable
+	if (row < 0 || row >= metadata_audit_table_entries.size()) {
+	    return null;
+	}
+
+	// First column: filename
+	if (col == 0) {
+	    ArrayList cell_values = new ArrayList();
+	    cell_values.add(((MetadataAuditTableEntry) metadata_audit_table_entries.get(row)).getFilename());
+	    return cell_values;
+	}
+
+	// Check values are reasonable
+	int metadata_element_index = col - 1;
+	if (metadata_element_index < 0 || metadata_element_index >= metadata_elements.size()) {
+	    return null;
+	}
+
+	ArrayList cell_values = new ArrayList();
+	MetadataAuditTableEntry metadata_audit_table_entry = (MetadataAuditTableEntry) metadata_audit_table_entries.get(row);
+	MetadataValueTreeNode[] metadata_value_tree_nodes = metadata_audit_table_entry.getMetadataValueTreeNodes(metadata_element_index);
+	if (metadata_value_tree_nodes != null) {
+	    for (int j = 0; j < metadata_value_tree_nodes.length; j++) {
+		cell_values.add(metadata_value_tree_nodes[j].toString());
+	    }
+	}
+
+	return cell_values;
+    }
+
+
+    public void rebuild(CollectionTreeNode[] file_nodes)
+    {
+	metadata_elements = MetadataSetManager.getEveryMetadataSetElement();
+	metadata_audit_table_entries.clear();
+
+	this.file_nodes = file_nodes;
+	if (file_nodes != null && file_nodes.length > 0) {
+	    // Create model builder
+	    MetadataAuditTableModelBuilder builder = new MetadataAuditTableModelBuilder();
+	    builder.run();
+	}
+    }
+
+
+    private class MetadataAuditTableModelBuilder
+    {
+	// Build a list of MetadataAuditTableEntries that represent the metadata asssigned to the selected files
+	public void run()
+	{
+	    // Process each of the selected files in turn
+	    for (int i = 0; i < file_nodes.length; i++) {
+		File current_file = file_nodes[i].getFile();
+
+		// Get the metadata assigned to this file
+		ArrayList current_file_metadata = MetadataXMLFileManager.getMetadataAssignedToFile(current_file);
+
+		// Get the extracted metadata for this file, if desired
+		if (Configuration.get("general.view_extracted_metadata", Configuration.COLLECTION_SPECIFIC) == true) {
+		    current_file_metadata.addAll(DocXMLFileManager.getMetadataExtractedFromFile(current_file));
+		}
+
+		MetadataAuditTableEntry metadata_audit_table_entry = new MetadataAuditTableEntry(current_file, current_file_metadata);
+		metadata_audit_table_entries.add(metadata_audit_table_entry);
+	    }
+	}
+    }
+
+
+    /** This class represents one (file, metadata) pair */
+    private class MetadataAuditTableEntry
+    {
+	private File file = null;
+	private ArrayList[] metadata_values = null;
+
+
+	public MetadataAuditTableEntry(File file, ArrayList metadata_values_list)
+	{
+	    this.file = file;
+
+	    metadata_values = new ArrayList[metadata_elements.size()];
+	    for (int i = 0; i < metadata_values.length; i++) {
+		metadata_values[i] = new ArrayList();
+	    }
+
+	    for (int i = 0; i < metadata_values_list.size(); i++) {
+		MetadataValue metadata_value = (MetadataValue) metadata_values_list.get(i);
+		MetadataElement metadata_element = metadata_value.getMetadataElement();
+
+		// Find the right column to put the value in
+		for (int j = 0; j < metadata_elements.size(); j++) {
+		    if (metadata_element.equals(metadata_elements.get(j))) {
+			metadata_values[j].add(metadata_value);
+			break;
+		    }
+		}
+	    }
+	}
+
+
+	public String getFilename()
+	{
+	    return file.getName();
+	}
+
+
+ 	public MetadataValueTreeNode[] getMetadataValueTreeNodes(int metadata_element_index)
+ 	{
+	    ArrayList metadata_values_list = metadata_values[metadata_element_index];
+ 	    if (metadata_values_list == null) {
+ 		return null;
+ 	    }
+
+	    MetadataValueTreeNode[] metadata_value_tree_nodes = new MetadataValueTreeNode[metadata_values_list.size()];
+	    for (int i = 0; i < metadata_values_list.size(); i++) {
+		metadata_value_tree_nodes[i] = ((MetadataValue) metadata_values_list.get(i)).getMetadataValueTreeNode();
+	    }
+
+ 	    return metadata_value_tree_nodes;
+ 	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataChangedListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataChangedListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataChangedListener.java	(revision 31635)
@@ -0,0 +1,37 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+
+
+public interface MetadataChangedListener
+{
+    /** Notification that the metadata assigned to the collection has changed. */
+    public void metadataChanged(CollectionTreeNode[] file_nodes);
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataElement.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataElement.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataElement.java	(revision 31635)
@@ -0,0 +1,231 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.File;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.util.XMLTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.w3c.dom.*;
+
+
+/** This class represents one Metadata element */
+public class MetadataElement
+{
+    // Language-independent metadata element name, with namespace (eg. dc.Subject)
+    private String metadata_element_name_full = null;
+    private Element metadata_element_element = null;
+    private MetadataValueTreeModel metadata_value_tree_model = null;
+    /* this is true if the metadata element comes with a pre defined set of values */
+    private boolean predefined_values = false;
+    /* This restricts the user to only the predefined values */
+    private boolean restricted_values = false;
+  /* can we have more than one value for this metadata element per document? */
+  /* default, true = yes */
+  private boolean accumulating = true;
+    public MetadataElement(String metadata_element_name_full, Element metadata_element_element)
+    {
+	this.metadata_element_name_full = metadata_element_name_full;
+	this.metadata_element_element = metadata_element_element;
+	this.metadata_value_tree_model = new MetadataValueTreeModel(this);
+	if (metadata_element_element != null) {
+	    if (metadata_element_element.getAttribute(StaticStrings.PREDEFINED_METADATA_ATTRIBUTE).equals("true")) {
+		predefined_values = true;
+		loadOptionList();
+		if (metadata_element_element.getAttribute(StaticStrings.RESTRICTED_METADATA_ATTRIBUTE).equals("true")) {
+		    // cant have restricted values if no predefined values
+		    restricted_values = true;
+		}
+	    }
+	    if (metadata_element_element.getAttribute("accumulating").equals("false")) {
+	      accumulating = false;
+	    }
+	}
+    }
+	
+    // do this better
+    protected boolean loadOptionList() {
+	NodeList values = metadata_element_element.getElementsByTagName("Value");
+	for (int i=0; i<values.getLength(); i++) {
+	    String val = XMLTools.getValue(values.item(i));
+	    addMetadataValue(val);
+	}
+	return true;
+    }
+    public MetadataValueTreeNode addMetadataValue(String metadata_value)
+    {
+	return metadata_value_tree_model.addMetadataValue(metadata_value);
+    }
+
+
+    public String getAttribute(String attribute_name, String language_code)
+    {
+	// Some extracted metadata elements may not be in ex.mds, and so have no attributes
+	if (metadata_element_element == null) {
+	    return null;
+	}
+
+	Element parent_element = metadata_element_element;
+	boolean language_matches = false;
+
+	NodeList language_elements_nodelist = metadata_element_element.getElementsByTagName("Language");
+	for (int i = 0; i < language_elements_nodelist.getLength(); i++) {
+	    Element current_language_element = (Element) language_elements_nodelist.item(i);
+	    String current_language_element_code = current_language_element.getAttribute("code");
+	    if (current_language_element_code.equals(language_code)) {
+		// Found language; use it for the next step
+		parent_element = current_language_element;
+		language_matches = true;
+		break;
+	    }
+	}
+
+	NodeList attribute_elements_nodelist = parent_element.getElementsByTagName("Attribute");
+	for (int i = 0; i < attribute_elements_nodelist.getLength(); i++) {
+	    Element current_attribute_element = (Element) attribute_elements_nodelist.item(i);
+	    String current_attribute_element_name = current_attribute_element.getAttribute("name");
+	    if (!current_attribute_element_name.equals(attribute_name)) {
+		// Move on to the next Attribute element if the name doesn't match
+		continue;
+	    }
+
+	    // Check if the language matches
+	    String current_attribute_element_language = current_attribute_element.getAttribute("language");
+	    if (language_matches || current_attribute_element_language.equals(language_code)) {
+		// Found attribute; returns its value
+		return XMLTools.getElementTextValue(current_attribute_element);
+	    }
+	}
+
+	return null;
+    }
+
+
+    /** Returns language-dependent metadata element name, with namespace (eg. dc.Subject and Keywords) */ 
+    public String getDisplayName()
+    {
+	// now we use label, previously we used identifier
+	String metadata_element_display_name = MetadataTools.getMetadataElementAttribute(this, "label", Configuration.getLanguage(), "en");
+	if (metadata_element_display_name == null) {
+	    // try identifier
+	    metadata_element_display_name = MetadataTools.getMetadataElementAttribute(this, "identifier", Configuration.getLanguage(), "en");
+	}
+	// Root case (no subelements)
+	if (metadata_element_name_full.indexOf(MetadataTools.SUBELEMENT_SEPARATOR) == -1) {
+	    if (metadata_element_display_name != null) {
+		return getNamespace() + "." + metadata_element_display_name;
+	    }
+
+	    return metadata_element_name_full;
+	}
+
+	// Recursive case (hierarchical metadata elements)
+	else {
+	    String parent_metadata_element_name_full = metadata_element_name_full.substring(0, metadata_element_name_full.lastIndexOf(MetadataTools.SUBELEMENT_SEPARATOR));
+	    MetadataElement parent_metadata_element = MetadataTools.getMetadataElementWithName(parent_metadata_element_name_full);
+
+	    String parent_metadata_element_display_name = parent_metadata_element_name_full;
+	    if (parent_metadata_element != null) {
+		parent_metadata_element_display_name = parent_metadata_element.getDisplayName();
+	    }
+
+	    if (metadata_element_display_name != null) {
+		return parent_metadata_element_display_name + MetadataTools.SUBELEMENT_SEPARATOR + metadata_element_display_name;
+	    }
+
+	    return parent_metadata_element_display_name + metadata_element_name_full.substring(parent_metadata_element_name_full.length());
+	}
+    }
+
+
+    public MetadataValueTreeModel getMetadataValueTreeModel()
+    {
+	return metadata_value_tree_model;
+    }
+
+
+    public MetadataValueTreeNode getMetadataValueTreeNode(String metadata_value)
+    {
+	return metadata_value_tree_model.getMetadataValueTreeNode(metadata_value);
+    }
+
+
+    /** Returns language-independent metadata element name, with namespace (eg. dc.Subject) */
+    public String getFullName()
+    {
+	return metadata_element_name_full;
+    }
+
+
+    public String getNamespace()
+    {
+	return MetadataTools.getMetadataSetNamespace(metadata_element_name_full);
+    }
+
+
+    /** Returns language-independent metadata element name, *without* namespace (eg. Subject) */
+    public String getName()
+    {
+	return MetadataTools.getMetadataElementName(metadata_element_name_full);
+    }
+
+
+    public boolean isGSExtractedMetadataElement()
+    {
+	return getNamespace().equals(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE);
+    }
+
+    public boolean isExtractedMetadataElement()
+    {
+	return getNamespace().startsWith(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE);
+    }
+
+
+    public boolean isPredefined() {
+	return predefined_values;
+    }
+
+    public boolean isRestricted() {
+	return restricted_values;
+    }
+
+  public boolean isAccumulating() {
+    return accumulating;
+  }
+    public boolean isSubelement()
+    {
+	return (metadata_element_name_full.indexOf(MetadataTools.SUBELEMENT_SEPARATOR) != -1);
+    }
+
+
+    /** This is used to display the element in the metadata value table */
+    public String toString()
+    {
+	return getDisplayName();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataSet.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataSet.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataSet.java	(revision 31635)
@@ -0,0 +1,226 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.File;
+import java.util.*;
+import javax.swing.filechooser.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+
+/** This class represents one metadata set */
+public class MetadataSet
+{
+    private File metadata_set_file = null;
+    private ArrayList metadata_set_elements = new ArrayList();
+    private String metadata_set_namespace = null;
+
+
+    public MetadataSet(File metadata_set_file)
+    {
+	this.metadata_set_file = metadata_set_file;
+
+	// Parse the specified metadata set file
+	Document document = XMLTools.parseXMLFile(metadata_set_file);
+	if (document == null) {
+	    System.err.println("Error: Could not parse metadata set file " + metadata_set_file.getAbsolutePath());
+	    return;
+	}
+
+	// Read the metadata set's namespace
+	metadata_set_namespace = document.getDocumentElement().getAttribute("namespace");
+
+	// Read the Element elements in the metadata set
+	NodeList element_elements_nodelist = document.getElementsByTagName("Element");
+	for (int i = 0; i < element_elements_nodelist.getLength(); i++) {
+	    Element metadata_element_element = (Element) element_elements_nodelist.item(i);
+	    String metadata_element_name = metadata_element_element.getAttribute("name");
+
+	    // Deal with hierarchical metadata elements
+	    Element parent_metadata_element_element = (Element) metadata_element_element.getParentNode();
+	    while (parent_metadata_element_element.getTagName().equals("Element")) {
+		String parent_metadata_element_name = parent_metadata_element_element.getAttribute("name");
+		metadata_element_name = parent_metadata_element_name + MetadataTools.SUBELEMENT_SEPARATOR + metadata_element_name;
+		parent_metadata_element_element = (Element) parent_metadata_element_element.getParentNode();
+	    }
+
+	    String metadata_element_name_full = metadata_set_namespace + MetadataTools.NAMESPACE_SEPARATOR + metadata_element_name;
+	    metadata_set_elements.add(new MetadataElement(metadata_element_name_full, metadata_element_element));
+	}
+    }
+
+
+    public MetadataElement addMetadataElementForThisSession(String metadata_element_name)
+    {
+	String metadata_element_name_full = metadata_set_namespace + MetadataTools.NAMESPACE_SEPARATOR + metadata_element_name;
+	MetadataElement metadata_element = new MetadataElement(metadata_element_name_full, null);
+	metadata_set_elements.add(metadata_element);
+	return metadata_element;
+    }
+
+
+    public int compareMetadataElements(MetadataElement metadata_element_a, MetadataElement metadata_element_b)
+    {
+	// Extracted metadata is sorted alphabetically
+	if (metadata_set_namespace.equals(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE)) {
+	    return metadata_element_a.getName().compareTo(metadata_element_b.getName());
+	}
+
+	// Elements in metadata sets are sorted according to the other they are specified in the .mds file
+	return metadata_set_elements.indexOf(metadata_element_a) - metadata_set_elements.indexOf(metadata_element_b);
+    }
+
+    /** we consider MetadataSets to be equal if their names and namespaces match */
+    public boolean equals(Object obj) {
+	if (obj instanceof MetadataSet) {
+	    return toString().equals(obj.toString());
+	}
+	return false;
+    }
+
+
+    public String getAttribute(String attribute_name, String language_code)
+    {
+	// Parse the specified metadata set file
+	Document document = XMLTools.parseXMLFile(metadata_set_file);
+	if (document == null) {
+	    System.err.println("Error: Could not parse metadata set file " + metadata_set_file.getAbsolutePath());
+	    return null;
+	}
+
+	Element document_element = document.getDocumentElement();
+	Element parent_element = document_element;
+	boolean language_matches = false;
+
+	NodeList setlanguage_elements_nodelist = document_element.getElementsByTagName("SetLanguage");
+	for (int i = 0; i < setlanguage_elements_nodelist.getLength(); i++) {
+	    Element current_setlanguage_element = (Element) setlanguage_elements_nodelist.item(i);
+	    String current_setlanguage_element_code = current_setlanguage_element.getAttribute("code");
+	    if (current_setlanguage_element_code.equals(language_code)) {
+		// Found language; use it for the next step
+		parent_element = current_setlanguage_element;
+		language_matches = true;
+		break;
+	    }
+	}
+
+	NodeList attribute_elements_nodelist = parent_element.getElementsByTagName(attribute_name);
+	for (int i = 0; i < attribute_elements_nodelist.getLength(); i++) {
+	    Element current_attribute_element = (Element) attribute_elements_nodelist.item(i);
+
+	    // Check if the language matches
+	    String current_attribute_element_language = current_attribute_element.getAttribute("language");
+	    if (language_matches || current_attribute_element_language.equals(language_code)) {
+		// Found attribute; returns its value
+		return XMLTools.getElementTextValue(current_attribute_element);
+	    }
+	}
+
+	return null;
+    }
+
+
+    public MetadataElement getMetadataElementWithDisplayName(String metadata_element_display_name_full)
+    {
+	// Find the desired metadata element in the list
+	for (int i = 0; i < metadata_set_elements.size(); i++) {
+	    MetadataElement metadata_element = (MetadataElement) metadata_set_elements.get(i);
+	    if (metadata_element.getDisplayName().equals(metadata_element_display_name_full)) {
+		return metadata_element;
+	    }
+	}
+
+	// Not found
+	return null;
+    }
+
+
+    public MetadataElement getMetadataElementWithName(String metadata_element_name)
+    {
+	// Find the desired metadata element in the list
+	for (int i = 0; i < metadata_set_elements.size(); i++) {
+	    MetadataElement metadata_element = (MetadataElement) metadata_set_elements.get(i);
+	    if (metadata_element.getName().equals(metadata_element_name)) {
+		return metadata_element;
+	    }
+	}
+
+	// Not found
+	return null;
+    }
+
+
+    public ArrayList getMetadataSetElements()
+    {
+	// We don't want anyone to modify the list, so return a copy of it
+	return (ArrayList) metadata_set_elements.clone();
+    }
+
+
+    public File getMetadataSetFile()
+    {
+	return metadata_set_file;
+    }
+
+
+    public String getNamespace()
+    {
+	return metadata_set_namespace;
+    }
+
+
+    /** This is used to display the metadata set in the metadata import mapping and explode metadata prompts */
+    public String toString()
+    {
+	return MetadataTools.getMetadataSetAttribute(this, "Name", Configuration.getLanguage(), "en") + " (" + metadata_set_namespace + ")";
+    }
+
+
+    static public class MetadataSetFileFilter
+	extends FileFilter
+    {
+	public boolean accept(File file)
+	{
+	    String file_name = file.getName().toLowerCase();
+	    if (file_name.endsWith(".mds") || file_name.indexOf(".") == -1) {
+		return true;
+	    }
+
+	    return false;
+	}
+
+
+	public String getDescription()
+	{
+	    return Dictionary.get("MetadataSet.Files");
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataSetManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataSetManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataSetManager.java	(revision 31635)
@@ -0,0 +1,214 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.gui.MetadataImportMappingPrompt;
+
+
+/** This class is a static class that manages the metadata sets */
+public class MetadataSetManager
+{
+    static public final String EXTRACTED_METADATA_NAMESPACE = "ex";
+    static public final String EXPLODED_METADATA_NAMESPACE = "exp";
+
+    static private ArrayList metadata_sets = new ArrayList();
+
+
+    static public void clearMetadataSets()
+    {
+	metadata_sets.clear();
+    }
+
+
+    static public int compareMetadataElements(MetadataElement metadata_element_a, MetadataElement metadata_element_b)
+    {
+	// First compare the namespaces of the two metadata elements
+	if (!metadata_element_a.getNamespace().equals(metadata_element_b.getNamespace())) {
+	  if (metadata_element_a.getNamespace().equals(EXTRACTED_METADATA_NAMESPACE)) {
+	    return 1;
+	  }
+	  if (metadata_element_b.getNamespace().equals(EXTRACTED_METADATA_NAMESPACE)) {
+	    return -1;
+	  }
+	  return metadata_element_a.getNamespace().compareTo(metadata_element_b.getNamespace());
+	}
+
+	// Otherwise compare the two elements within the one set
+	MetadataSet metadata_set = getMetadataSet(metadata_element_a.getNamespace());
+	return metadata_set.compareMetadataElements(metadata_element_a, metadata_element_b);
+    }
+
+
+    static public ArrayList getEveryMetadataSetElement()
+    {
+	ArrayList every_metadata_set_element = new ArrayList();
+
+	for (int i = 0; i < metadata_sets.size(); i++) {
+	    MetadataSet metadata_set = (MetadataSet) metadata_sets.get(i);
+	    every_metadata_set_element.addAll(metadata_set.getMetadataSetElements());
+	}
+
+	return every_metadata_set_element;
+    }
+
+
+    static public MetadataSet getMetadataSet(String metadata_set_namespace)
+    {
+	// Find the desired metadata set in the list
+	for (int i = 0; i < metadata_sets.size(); i++) {
+	    MetadataSet metadata_set = (MetadataSet) metadata_sets.get(i);
+	    if (metadata_set.getNamespace().equals(metadata_set_namespace)) {
+		return metadata_set;
+	    }
+	}
+
+	// Not found
+	return null;
+    }
+
+
+    static public ArrayList getMetadataSets()
+    {
+	// We don't want anyone to modify the list, so return a copy of it
+	return (ArrayList) metadata_sets.clone();
+    }
+
+
+    static public ArrayList listMetadataSets(File directory)
+    {
+	// Make sure the directory (metadata) exists
+	if (directory.exists() == false) {
+	    return null;
+	}
+
+	// Load just those .mds files in this directory, and return them
+	ArrayList directory_metadata_sets = new ArrayList();
+	File[] directory_files = directory.listFiles();
+	for (int i = 0; i < directory_files.length; i++) {
+	    File child_file = directory_files[i];
+	    if (!child_file.isDirectory() && child_file.getName().endsWith(".mds")) {
+		MetadataSet metadata_set = new MetadataSet(child_file);
+		directory_metadata_sets.add(metadata_set);
+	    }
+	}
+
+	return directory_metadata_sets;
+    }
+
+
+    static public void loadMetadataSets(File directory)
+    {
+	// Make sure the directory (metadata) exists
+	if (directory.exists() == false) {
+	    return;
+	}
+
+	// Look recursively at each subfile of the directory for .mds files
+	File[] directory_files = directory.listFiles();
+	for (int i = 0; i < directory_files.length; i++) {
+	    File child_file = directory_files[i];
+	    if (child_file.isDirectory()) {
+		loadMetadataSets(child_file);
+	    }
+	    else if (child_file.getName().endsWith(".mds")) {
+		loadMetadataSet(child_file);
+	    }
+	}
+    }
+
+
+    static public void loadMetadataSet(File metadata_set_file)
+    {
+	MetadataSet metadata_set = new MetadataSet(metadata_set_file);
+
+	// Ignore legacy crap
+	if (metadata_set.getNamespace().equals("hidden")) {
+	    return;
+	}
+
+	metadata_sets.add(metadata_set);
+    }
+
+
+    static public String mapUnloadedMetadataElement(String metadata_element_name_full)
+    {
+	// Check if we have an import mapping for this metadata element
+	String target_metadata_element_name_full = ProfileXMLFileManager.getMetadataElementFor(metadata_element_name_full);
+	if (target_metadata_element_name_full != null) {
+	    // Yes, so return it
+	    return target_metadata_element_name_full;
+	}
+
+	// If there are no metadata sets (except extracted) loaded then there is nothing to map the element into
+	if (metadata_sets.size() <= 1) {
+	    return null;
+	}
+
+	// Ask the user how they want to deal with this element
+	MetadataImportMappingPrompt metadata_import_mapping_prompt = new MetadataImportMappingPrompt(metadata_element_name_full);
+	int result = metadata_import_mapping_prompt.getResult();
+
+	// Add the element into an existing metadata set
+	if (result == MetadataImportMappingPrompt.ADD_BUTTON_PRESSED) {
+	    MetadataSet target_metadata_set = metadata_import_mapping_prompt.getSelectedMetadataSet();
+	    String metadata_element_name = MetadataTools.getMetadataElementName(metadata_element_name_full);
+	    target_metadata_set.addMetadataElementForThisSession(metadata_element_name);
+	    target_metadata_element_name_full = target_metadata_set.getNamespace() + "." + metadata_element_name;
+	}
+
+	// Replace the element with an element in an existing metadata set
+	if (result == MetadataImportMappingPrompt.MERGE_BUTTON_PRESSED) {
+	    MetadataElement target_metadata_element = metadata_import_mapping_prompt.getSelectedMetadataElement();
+	    target_metadata_element_name_full = target_metadata_element.getFullName();
+	}
+
+	// Ignore the element
+	if (result == MetadataImportMappingPrompt.IGNORE_BUTTON_PRESSED) {
+	    target_metadata_element_name_full = "";
+	}
+
+	// Store this import mapping for future elements with the same name
+	ProfileXMLFileManager.mapElement(metadata_element_name_full, target_metadata_element_name_full);
+
+	return target_metadata_element_name_full;
+    }
+
+
+    static public void unloadMetadataSet(MetadataSet metadata_set)
+    {
+	// Find the metadata set in the list of loaded sets, and remove it
+	for (int i = 0; i < metadata_sets.size(); i++) {
+	    if (metadata_set == (MetadataSet) metadata_sets.get(i)) {
+		metadata_sets.remove(i);
+		break;
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataTools.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataTools.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataTools.java	(revision 31635)
@@ -0,0 +1,189 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+import java.util.StringTokenizer;
+
+/** This class is a static class containing useful metadata functions */
+public class MetadataTools
+{
+    static final public String NAMESPACE_SEPARATOR = ".";
+    static final public String SUBELEMENT_SEPARATOR = "^";
+
+
+    static public String getDisplayNameForMetadataElementWithName(String metadata_element_name_full)
+    {
+	MetadataElement metadata_element = getMetadataElementWithName(metadata_element_name_full);
+	if (metadata_element != null) {
+	    return metadata_element.getDisplayName();
+	}
+
+	return metadata_element_name_full;
+    }
+
+
+    static public String getMetadataElementAttribute(MetadataElement metadata_element, String attribute_name, String language_code, String fallback_language_code)
+    {
+	String metadata_element_attribute = metadata_element.getAttribute(attribute_name, language_code);
+
+	// If the attribute isn't defined in the desired language, resort to the fallback
+	if (metadata_element_attribute == null && !language_code.equals(fallback_language_code)) {
+	    metadata_element_attribute = metadata_element.getAttribute(attribute_name, fallback_language_code);
+	}
+
+	return metadata_element_attribute;
+    }
+
+
+    static public String getMetadataElementName(String metadata_element_name_full)
+    {
+	// Full element name contains namespace
+	if (metadata_element_name_full.indexOf(NAMESPACE_SEPARATOR) != -1) {
+	    return metadata_element_name_full.substring(metadata_element_name_full.indexOf(NAMESPACE_SEPARATOR) + 1);
+	}
+
+	// No namespace
+	return metadata_element_name_full;      
+    }
+
+
+    static public MetadataElement getMetadataElementWithDisplayName(String metadata_element_display_name_full)
+    {
+	String metadata_set_namespace = getMetadataSetNamespace(metadata_element_display_name_full);
+	MetadataSet metadata_set = MetadataSetManager.getMetadataSet(metadata_set_namespace);
+	if (metadata_set == null) {
+	    return null;
+	}
+
+	return metadata_set.getMetadataElementWithDisplayName(metadata_element_display_name_full);
+    }
+
+
+    static public MetadataElement getMetadataElementWithName(String metadata_element_name_full)
+    {
+	String metadata_set_namespace = getMetadataSetNamespace(metadata_element_name_full);
+	MetadataSet metadata_set = MetadataSetManager.getMetadataSet(metadata_set_namespace);
+	if (metadata_set == null) {
+	    return null;
+	}
+
+	String metadata_element_name = getMetadataElementName(metadata_element_name_full);
+	return metadata_set.getMetadataElementWithName(metadata_element_name);
+    }
+
+
+    static public String getMetadataSetAttribute(MetadataSet metadata_set, String attribute_name, String language_code, String fallback_language_code)
+    {
+	String metadata_set_attribute = metadata_set.getAttribute(attribute_name, language_code);
+
+	// If the attribute isn't defined in the desired language, resort to the fallback
+	if (metadata_set_attribute == null && !language_code.equals(fallback_language_code)) {
+	    metadata_set_attribute = metadata_set.getAttribute(attribute_name, fallback_language_code);
+	}
+
+	return metadata_set_attribute;
+    }
+
+
+    static public String getMetadataSetNamespace(String metadata_element_name_full)
+    {
+	// Full element name contains namespace
+	if (metadata_element_name_full.indexOf(NAMESPACE_SEPARATOR) != -1) {
+	    return metadata_element_name_full.substring(0, metadata_element_name_full.indexOf(NAMESPACE_SEPARATOR));
+	}
+
+	// No namespace
+	return "";
+    }
+
+
+    static public String getRegularExpressionThatMatchesFilePath(String file_path)
+    {
+	// Convert the file path into a regular expression that will match it
+	String file_path_regexp = file_path;
+	file_path_regexp = file_path_regexp.replaceAll("\\.", "\\\\.");
+	file_path_regexp = file_path_regexp.replaceAll("\\(", "\\\\(");
+	file_path_regexp = file_path_regexp.replaceAll("\\)", "\\\\)");
+	file_path_regexp = file_path_regexp.replaceAll("\\[", "\\\\[");
+	file_path_regexp = file_path_regexp.replaceAll("\\]", "\\\\]");
+	file_path_regexp = file_path_regexp.replaceAll("\\{", "\\\\{");
+	file_path_regexp = file_path_regexp.replaceAll("\\}", "\\\\}");
+	file_path_regexp = file_path_regexp.replaceAll("\\+", "\\\\+");
+	return file_path_regexp;
+    }
+
+    static public boolean TO_DISPLAY_NAMES = true;
+    static public boolean FROM_DISPLAY_NAMES = false;
+
+    static public String convertMetadataElementListNames(String value_str, boolean to_display) {
+	if (value_str == null || value_str.length () == 0) {
+	    return value_str;
+	}
+    
+    // final true arg to return delims as tokens
+    StringTokenizer string_tokenizer = new StringTokenizer (value_str, ",;/", true);
+    StringBuffer value_buffer = new StringBuffer ();
+    while (string_tokenizer.hasMoreElements ()) {
+      String raw_token = (String) string_tokenizer.nextElement ();
+      String token = raw_token.trim ();
+      boolean modified_token = false;
+      // not a delimiter token
+      if (!raw_token.equals(",") && !raw_token.equals(";") && !raw_token.equals("/")) {
+	  MetadataElement meta_elem = null;
+	  if (to_display) {
+	     meta_elem = getMetadataElementWithName(token);
+	     if (meta_elem !=null) {
+		 token = meta_elem.getDisplayName();
+		 modified_token = true;
+	     }
+	  } else {
+	      meta_elem = getMetadataElementWithDisplayName(token);
+	      
+	      if (meta_elem != null) {
+		  token = meta_elem.getFullName ();
+		  modified_token = true;
+	      }
+	  }
+      }
+      if (modified_token) {
+	value_buffer.append (token);
+      } else {
+	// we may have had whitespace etc that was part of the string
+	value_buffer.append (raw_token);
+      }
+    }
+    
+    
+    if(value_buffer.length () == 0) {
+      return "";
+    }
+    return value_buffer.toString();
+    }
+ 
+
+}
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValue.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValue.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValue.java	(revision 31635)
@@ -0,0 +1,141 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+
+
+/** This class represents one metadata (element, value) pair */
+public class MetadataValue
+    implements Comparable
+{
+    /** The folder that this piece of metadata was inherited from, if applicable */
+    private File folder_metadata_inherited_from = null;
+    private MetadataElement metadata_element = null;
+    private MetadataValueTreeNode metadata_value_tree_node = null;
+  // this will be set to true if the metadata element definition says accumulating = false.
+  private boolean must_not_accumulate = false;
+    private boolean is_accumulating_metadata = false;
+    /** Whether this metadata applies to only one file (ie. there are no wildcards in the FileSet value) */
+    private boolean is_one_file_only_metadata = true;
+
+
+    public MetadataValue(MetadataElement metadata_element, MetadataValueTreeNode metadata_value_tree_node)
+    {
+	this.metadata_element = metadata_element;
+	this.metadata_value_tree_node = metadata_value_tree_node;
+	if (metadata_element != null) {
+	  if (!metadata_element.isAccumulating()) {
+	    must_not_accumulate = true;
+	  }
+	}
+    }
+
+
+    public int compareTo(Object metadata_value_object)
+    {
+	MetadataValue metadata_value = (MetadataValue) metadata_value_object;
+
+	// First compare the metadata elements
+	int c = MetadataSetManager.compareMetadataElements(this.getMetadataElement(), metadata_value.getMetadataElement());
+	if (c != 0) {
+	    return c;
+	}
+
+	// Then use the metadata values
+	return this.getFullValue().compareTo(metadata_value.getFullValue());
+    }
+
+
+    public File getFolderMetadataInheritedFrom()
+    {
+	return folder_metadata_inherited_from;
+    }
+
+
+    public MetadataElement getMetadataElement()
+    {
+	return metadata_element;
+    }
+
+
+    public String getFullValue()
+    {
+	return metadata_value_tree_node.getFullValue();
+    }
+
+
+    public String getValue()
+    {
+	return metadata_value_tree_node.getValue();
+    }
+
+
+    public MetadataValueTreeNode getMetadataValueTreeNode()
+    {
+	return metadata_value_tree_node;
+    }
+
+
+    public void inheritsMetadataFromFolder(File folder_metadata_inherited_from)
+    {
+	this.folder_metadata_inherited_from = folder_metadata_inherited_from;
+    }
+
+
+    public boolean isAccumulatingMetadata()
+    {
+	return is_accumulating_metadata;
+    }
+
+
+    public boolean isInheritedMetadata()
+    {
+	return (folder_metadata_inherited_from != null);
+    }
+
+
+    public boolean isOneFileOnlyMetadata()
+    {
+	return is_one_file_only_metadata;
+    }
+
+
+    public void setIsAccumulatingMetadata(boolean is_accumulating_metadata)
+    {
+      if (!must_not_accumulate) {
+	this.is_accumulating_metadata = is_accumulating_metadata;
+      } 
+    }
+
+
+    public void setIsOneFileOnlyMetadata(boolean is_one_file_only_metadata)
+    {
+	this.is_one_file_only_metadata = is_one_file_only_metadata;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTableEntry.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTableEntry.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTableEntry.java	(revision 31635)
@@ -0,0 +1,69 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+/** This class represents one entry in the metadata value table. */
+public class MetadataValueTableEntry
+    extends MetadataValue
+{
+    private int occurrences = 0;
+
+
+    public MetadataValueTableEntry(MetadataElement metadata_element, MetadataValueTreeNode metadata_value_tree_node)
+    {
+	super(metadata_element, metadata_value_tree_node);
+
+	occurrences = 1;
+    }
+
+
+    public MetadataValueTableEntry(MetadataValue metadata_value)
+    {
+	this(metadata_value.getMetadataElement(), metadata_value.getMetadataValueTreeNode());
+	inheritsMetadataFromFolder(metadata_value.getFolderMetadataInheritedFrom());
+	setIsOneFileOnlyMetadata(metadata_value.isOneFileOnlyMetadata());
+    }
+
+
+    public void anotherOccurrence()
+    {
+	occurrences++;
+    }
+
+
+    public int getOccurrences()
+    {
+	return occurrences;
+    }
+
+
+    public void setOccurrences(int occurrences)
+    {
+	this.occurrences = occurrences;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTableModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTableModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTableModel.java	(revision 31635)
@@ -0,0 +1,502 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.table.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.gui.WarningDialog;
+
+
+public class MetadataValueTableModel
+    extends AbstractTableModel
+{
+    /** It is not expected that this class will ever be serialized */
+    private static final long serialVersionUID = 0L;
+
+    /** The CollectionTreeNodes this model is built for */
+    private CollectionTreeNode[] file_nodes = null;
+    /** The list of MetadataValueTableEntries in the table */
+    private ArrayList metadata_value_table_entries = new ArrayList();
+
+    static final private String[] COLUMN_NAMES = { "", Dictionary.get("Metadata.Element"), Dictionary.get("Metadata.Value") };
+
+
+    public int addBlankRowForMetadataElement(MetadataElement metadata_element)
+    {
+	MetadataValueTableModelBuilder metadata_value_table_model_builder = new MetadataValueTableModelBuilder();
+	MetadataValue blank_metadata_value = new MetadataValue(metadata_element, new MetadataValueTreeNode(""));
+	return metadata_value_table_model_builder.insertMetadataValue(blank_metadata_value);
+    }
+
+
+    public int findMetadataValueTableEntryToSelect(MetadataValueTableEntry metadata_value_table_entry)
+    {
+	MetadataElement metadata_element = metadata_value_table_entry.getMetadataElement();
+
+	// Find the correct entry to select
+	for (int i = 0; i < metadata_value_table_entries.size(); i++) {
+	    MetadataValueTableEntry current_metadata_value_table_entry = (MetadataValueTableEntry) metadata_value_table_entries.get(i);
+	    int element_comparison = MetadataSetManager.compareMetadataElements(current_metadata_value_table_entry.getMetadataElement(), metadata_element);
+
+	    // We've found the right element, so check if the value already exists
+	    if (element_comparison == 0) {
+		int value_comparison = current_metadata_value_table_entry.compareTo(metadata_value_table_entry);
+		if (value_comparison == 0) {
+		    // Entry found!
+		    return i;
+		}
+	    }
+
+	    // We've just gone past the correct entry to select
+	    if (element_comparison > 0) {
+		return i - 1;
+	    }
+	}
+
+	// Have to select the last entry
+	return metadata_value_table_entries.size() - 1;
+    }
+
+
+    public Class getColumnClass(int col)
+    {
+	return getValueAt(0, col).getClass();
+    }
+
+
+    /** Returns the number of columns in this table. */
+    public int getColumnCount()
+    {
+	return COLUMN_NAMES.length;
+    }
+
+
+    /** Retrieves the name of the specified column. */
+    public String getColumnName(int col)
+    {
+	return COLUMN_NAMES[col];
+    }
+
+
+    /* Called to retrieve the MetadataValueTableEntry at a certain row. Usually caused by the user selecting a row in the table. It is synchronized so that the model doesn't up and change while we're trying to retrieve the indicated element. */
+    public synchronized MetadataValueTableEntry getMetadataValueTableEntry(int row)
+    {
+	if (row >= 0 && row < metadata_value_table_entries.size()) {
+	    return (MetadataValueTableEntry) metadata_value_table_entries.get(row);
+	}
+
+	return null;
+    }
+
+
+    /** Returns the number of rows in this table. */
+    public int getRowCount()
+    {
+	return metadata_value_table_entries.size();
+    }
+
+
+    /** Returns the cell value at a given row and column as an Object. */
+    public Object getValueAt(int row, int col)
+    {
+	// Check values are reasonable
+	if (row < 0 || row >= metadata_value_table_entries.size() || col < 0 || col >= COLUMN_NAMES.length) {
+	    return null;
+	}
+
+	MetadataValueTableEntry metadata_value_table_entry = (MetadataValueTableEntry) metadata_value_table_entries.get(row);
+	
+	if(metadata_value_table_entry == null) {
+		System.err.println("\n@@@@@ MetadataValueTableModel.getValueAt(): metadata_value_table_entry is unexpectedly null!\n");
+		return null;
+	}
+	
+	if (col == 0 && metadata_value_table_entry.isInheritedMetadata()) {
+	    return metadata_value_table_entry.getFolderMetadataInheritedFrom();
+	}
+
+	if (col == 1) {
+	    return metadata_value_table_entry.getMetadataElement();
+	}
+
+	if (col == 2)
+	{
+		return decodeHTMLEntities(metadata_value_table_entry.getFullValue());
+	}
+
+	return null;
+	}
+	
+	public String decodeHTMLEntities(String text)
+	{
+		text = text.replace("&amp;#40;", "(");
+		text = text.replace("&amp;#41;", ")");
+		text = text.replace("&amp;#44;", ",");
+		text = text.replace("&amp;#60;", "<");
+		text = text.replace("&amp;#62;", ">");
+		text = text.replace("&amp;#91;", "[");
+		text = text.replace("&amp;#93;", "]");
+		text = text.replace("&amp;#123;", "{");
+		text = text.replace("&amp;#125;", "}");
+		return text;
+	}
+
+    public boolean isCellEditable(int row, int col)
+    {
+	// The inherited and element columns are never editable
+	if (col < 2) {
+	    return false;
+	}
+
+	// Extracted and inherited metadata is not editable
+	MetadataValueTableEntry metadata_value_table_entry = (MetadataValueTableEntry) metadata_value_table_entries.get(row);
+	if (metadata_value_table_entry.getMetadataElement().isExtractedMetadataElement() || metadata_value_table_entry.isInheritedMetadata()) {
+	    return false;
+	}
+
+	return true;
+    }
+
+
+    /** Determine if the given metadata is common to all selected file nodes. */
+    public boolean isCommon(MetadataValueTableEntry metadata_value_table_entry)
+    {
+	return (file_nodes != null && metadata_value_table_entry.getOccurrences() == file_nodes.length);
+    }
+
+
+    /** Determine if the given metadata is common to all selected file nodes. */
+    public boolean isCommon(int row)
+    {
+	if (row >= 0 && row < metadata_value_table_entries.size()) {
+	    return isCommon((MetadataValueTableEntry) metadata_value_table_entries.get(row));
+	}
+
+	return false;
+    }
+
+
+    public void rebuild(CollectionTreeNode[] file_nodes)
+    {
+	this.file_nodes = file_nodes;
+	metadata_value_table_entries.clear();
+
+	// Collection is in a state of flux
+	if (!Gatherer.c_man.ready()) {
+	    return;
+	}
+
+	if (file_nodes == null || file_nodes.length == 0) {
+	    return;
+	}
+
+	// Create model builder
+	MetadataValueTableModelBuilder builder = new MetadataValueTableModelBuilder();
+	builder.run();
+    }
+
+
+    public void setValueAt(Object new_metadata_value, int row, int col)
+    {
+	MetadataValueTableEntry metadata_value_table_entry = getMetadataValueTableEntry(row);
+	if(metadata_value_table_entry == null) {
+		System.err.println("\n@@@@@ MetadataValueTableModel.setValueAt(): metadata_value_table_entry is unexpectedly null!\n");
+		return;
+	}
+	
+	// If nothing has changed no action is necessary
+	String old_metadata_value = metadata_value_table_entry.getFullValue();
+	if (new_metadata_value.equals(old_metadata_value)) {
+	    return;
+	}
+
+	// Lock the interface so nothing can be changed while the metadata edit is being processed
+	Gatherer.g_man.wait(true);
+
+	
+	// Check for a restricted metadata element
+	MetadataElement metadata_element = metadata_value_table_entry.getMetadataElement();
+	if (!new_metadata_value.equals("") && metadata_element.isRestricted()) {
+	    if (metadata_element.getMetadataValueTreeNode((String)new_metadata_value)==null) {
+		WarningDialog dialog = new WarningDialog("warning.InvalidMetadata", Dictionary.get("InvalidMetadata.Title"), Dictionary.get("InvalidMetadata.Message"), null, false);
+		int dialog_result = dialog.display();
+		dialog.dispose();
+		Gatherer.g_man.wait(false);
+		return;
+		
+	    }
+	}
+	
+	// Metadata value added
+	if (old_metadata_value.equals("") && !new_metadata_value.equals("")) {    
+	    // If we're adding metadata to folders display the warning
+ 	    if (!file_nodes[0].isLeaf()) {
+ 		WarningDialog dialog = new WarningDialog("warning.DirectoryLevelMetadata", Dictionary.get("DirectoryLevelMetadata.Title"), Dictionary.get("DirectoryLevelMetadata.Message"), null, true);
+ 		int dialog_result = dialog.display();
+ 		dialog.dispose();
+ 		if (dialog_result != JOptionPane.OK_OPTION) {
+		    Gatherer.g_man.wait(false);
+ 		    return;
+ 		}
+ 	    }
+
+	    MetadataValueTreeNode metadata_value_tree_node = metadata_element.addMetadataValue((String) new_metadata_value);
+	    MetadataValue metadata_value = new MetadataValue(metadata_element, metadata_value_tree_node);
+	    metadata_value.setIsAccumulatingMetadata(true);		
+	    (new AppendMetadataTask(metadata_value)).run();
+	}
+
+	// Metadata value removed
+	else if (!old_metadata_value.equals("") && new_metadata_value.equals("")) {
+	    (new RemoveMetadataTask(metadata_value_table_entry)).run();
+	}
+
+	// Metadata value replaced
+	else {
+	    MetadataValueTreeNode metadata_value_tree_node = metadata_element.addMetadataValue((String) new_metadata_value);
+	    MetadataValue metadata_value = new MetadataValue(metadata_element, metadata_value_tree_node);
+	    metadata_value.setIsAccumulatingMetadata(!metadata_value_table_entry.isInheritedMetadata());
+	    (new ReplaceMetadataTask(metadata_value_table_entry, metadata_value)).run();
+	}
+    }
+
+
+    private class AppendMetadataTask
+	// extends Thread
+    {
+	private MetadataValue metadata_value = null;
+
+	private AppendMetadataTask(MetadataValue metadata_value)
+	{
+	    this.metadata_value = metadata_value;
+	}
+
+	public void run()
+	{
+	    try {
+		// Edit metadata.xml files to add the metadata
+		MetadataXMLFileManager.addMetadata(file_nodes, metadata_value);
+	    }
+	    catch (Exception exception) {
+		// We need to catch any exceptions here so the interface is unlocked below
+		DebugStream.printStackTrace(exception);
+	    }
+
+	    // Operation finished, so turn busy cursor off and unlock interface
+	    Gatherer.g_man.wait(false);
+	}
+    }
+
+
+    private class ReplaceMetadataTask
+	// extends Thread
+    {
+	private MetadataValueTableEntry selected_metadata_value_table_entry = null;
+	private MetadataValue metadata_value = null;
+
+	private ReplaceMetadataTask(MetadataValueTableEntry selected_metadata_value_table_entry, MetadataValue metadata_value)
+	{
+	    this.selected_metadata_value_table_entry = selected_metadata_value_table_entry;
+	    this.metadata_value = metadata_value;
+	}
+
+	public void run()
+	{
+	    try {
+		// Edit metadata.xml files to replace the metadata
+		MetadataXMLFileManager.replaceMetadata(file_nodes, selected_metadata_value_table_entry, metadata_value);
+	    }
+	    catch (Exception exception) {
+		// We need to catch any exceptions here so the interface is unlocked below
+		DebugStream.printStackTrace(exception);
+	    }
+
+	    // Operation finished, so turn busy cursor off and unlock interface
+	    Gatherer.g_man.wait(false);
+	}
+    }
+
+
+    private class RemoveMetadataTask
+	// extends Thread
+    {
+	private MetadataValueTableEntry selected_metadata_value_table_entry = null;
+
+	private RemoveMetadataTask(MetadataValueTableEntry selected_metadata_value_table_entry)
+	{
+	    this.selected_metadata_value_table_entry = selected_metadata_value_table_entry;
+	}
+
+	public void run()
+	{
+	    try {
+		// Edit metadata.xml files to remove the metadata
+		MetadataXMLFileManager.removeMetadata(file_nodes, selected_metadata_value_table_entry);
+	    }
+	    catch (Exception exception) {
+		// We need to catch any exceptions here so the interface is unlocked below
+		DebugStream.printStackTrace(exception);
+	    }
+
+	    // Operation finished, so turn busy cursor off and unlock interface
+	    Gatherer.g_man.wait(false);
+	}
+    }
+
+
+    private class MetadataValueTableModelBuilder
+    {
+	public void run()
+	{
+	    // System.err.println("Building MetadataValueTableModel...");
+
+	    // Build a list of MetadataValueTableEntries that represent the metadata asssigned to the selected files
+	    boolean hid_extracted_metadata = false;
+	    ArrayList metadata_elements_seen = new ArrayList();
+
+	    // Process each of the selected files in turn
+	    for (int i = 0; i < file_nodes.length; i++) {
+		File current_file = file_nodes[i].getFile();
+
+		// Get the metadata assigned to this file
+		ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedToFile(current_file);
+		for (int j = 0; j < assigned_metadata.size(); j++) {
+		    MetadataValue metadata_value = (MetadataValue) assigned_metadata.get(j);
+		    MetadataElement metadata_element = metadata_value.getMetadataElement();
+
+		    // Insert this metadata value into the table, unless it already exists (in which case increment its count)
+		    insertMetadataValue(metadata_value);
+
+		    // Remember we have seen this metadata element
+		    if (metadata_elements_seen.contains(metadata_element) == false) {
+			metadata_elements_seen.add(metadata_element);
+		    }
+		}
+
+		// Get the extracted metadata for this file, if desired
+		if (Configuration.get("general.view_extracted_metadata", Configuration.COLLECTION_SPECIFIC) == true) {
+		    ArrayList extracted_metadata = DocXMLFileManager.getMetadataExtractedFromFile(current_file);
+		    for (int k = 0; k < extracted_metadata.size(); k++) {
+			MetadataValue metadata_value = (MetadataValue) extracted_metadata.get(k);
+
+			// Insert this metadata value into the table, unless it already exists (in which case increment its count)
+			insertMetadataValue(metadata_value);
+		    }
+		}
+	    }
+
+	    // Make sure each non-extracted metadata element appears in the table (even if blank)
+	    ArrayList every_metadata_set_element = MetadataSetManager.getEveryMetadataSetElement();
+	    for (int i = 0; i < every_metadata_set_element.size(); i++) {
+		MetadataElement metadata_element = (MetadataElement) every_metadata_set_element.get(i);
+
+		// If we haven't seen this metadata element and it isn't extracted, add it now
+		if (!metadata_elements_seen.contains(metadata_element) && !metadata_element.isExtractedMetadataElement()) {
+		    MetadataValueTableEntry metadata_value_table_entry = new MetadataValueTableEntry(metadata_element, new MetadataValueTreeNode(""));
+
+		    // Blank metadata is common to all selected files (otherwise it wouldn't be blank!)
+		    metadata_value_table_entry.setOccurrences(file_nodes.length);
+
+		    // Add it to the table
+		    insertMetadataValueTableEntry(metadata_value_table_entry);
+		}
+	    }
+
+	    // If extracted metadata was hidden, display the warning
+	    if (hid_extracted_metadata) {
+		showExtractedMetadataWarning();
+	    }
+	}
+
+
+	/** Inserts the new metadata value into the table */
+	private int insertMetadataValue(MetadataValue metadata_value)
+	{
+	    return insertMetadataValueTableEntry(new MetadataValueTableEntry(metadata_value));
+	}
+
+
+	/** Inserts the new metadata value table entry into the table */
+	private int insertMetadataValueTableEntry(MetadataValueTableEntry metadata_value_table_entry)
+	{
+	    MetadataElement metadata_element = metadata_value_table_entry.getMetadataElement();
+
+	    // Find the correct place to insert the table entry
+	    for (int i = 0; i < metadata_value_table_entries.size(); i++) {
+		MetadataValueTableEntry current_metadata_value_table_entry = (MetadataValueTableEntry) metadata_value_table_entries.get(i);
+		int element_comparison = MetadataSetManager.compareMetadataElements(current_metadata_value_table_entry.getMetadataElement(), metadata_element);
+
+		// We've found the right element, so check if the value already exists
+		if (element_comparison == 0) {
+		    int value_comparison = current_metadata_value_table_entry.compareTo(metadata_value_table_entry);
+		    if (value_comparison == 0) {
+			// Entry already exists, so increment count (except for blank entries)
+			if (!metadata_value_table_entry.getFullValue().equals("")) {
+			    current_metadata_value_table_entry.anotherOccurrence();
+			}
+			return i;
+		    }
+		}
+
+		// Found insertion point
+		if (element_comparison > 0) {
+		    metadata_value_table_entries.add(i, metadata_value_table_entry);
+		    fireTableRowsInserted(i, i);
+		    return i;
+		}
+	    }
+
+	    // Must go at the end of the table
+	    metadata_value_table_entries.add(metadata_value_table_entry);
+	    fireTableRowsInserted(metadata_value_table_entries.size() - 1, metadata_value_table_entries.size() - 1);
+	    return metadata_value_table_entries.size() - 1;
+	}
+
+
+	private void showExtractedMetadataWarning()
+	{
+	    Runnable task = new Runnable() {
+		    public void run() {
+			WarningDialog dialog = new WarningDialog("warning.ExtractedMetadata", Dictionary.get("ExtractedMetadata.Title"), Dictionary.get("ExtractedMetadata.Message"), null, false);
+			dialog.display();
+			dialog.dispose();
+			dialog = null;
+		    }
+		};
+	    SwingUtilities.invokeLater(task);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTreeModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTreeModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTreeModel.java	(revision 31635)
@@ -0,0 +1,137 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+import java.util.*;
+import javax.swing.tree.*;
+
+
+/** This class represents all the metadata values for one metadata element */
+public class MetadataValueTreeModel
+    extends DefaultTreeModel
+{
+    public MetadataValueTreeModel(MetadataElement metadata_element)
+    {
+	// The root node of the value tree is the MetadataElement it represents
+	super(new MetadataValueTreeNode(metadata_element));
+    }
+
+
+    public MetadataValueTreeNode addMetadataValue(String metadata_value)
+    {
+	return addMetadataValueInternal((MetadataValueTreeNode) root, metadata_value);
+    }
+
+
+    private MetadataValueTreeNode addMetadataValueInternal(MetadataValueTreeNode parent_metadata_value_tree_node, String metadata_value_remaining)
+    {
+	// Split the metadata value into a hierarchy
+	int split_point = metadata_value_remaining.indexOf(MetadataValueTreeNode.METADATA_VALUE_TREE_NODE_HIERARCHY_TOKEN);
+	String metadata_value = ((split_point == -1) ? metadata_value_remaining : metadata_value_remaining.substring(0, split_point));
+	metadata_value_remaining = ((split_point == -1) ? "" : metadata_value_remaining.substring(split_point + 1));
+
+	// Add the value into the tree in the correct place (sorted)
+	MetadataValueTreeNode metadata_value_tree_node = null;
+	for (int i = 0; i < parent_metadata_value_tree_node.getChildCount(); i++) {
+	    MetadataValueTreeNode child_metadata_value_tree_node = (MetadataValueTreeNode) parent_metadata_value_tree_node.getChildAt(i);
+	    int c = metadata_value.compareTo(child_metadata_value_tree_node.getValue());
+
+	    // Insert node before existing value
+	    if (c < 0) {
+		metadata_value_tree_node = new MetadataValueTreeNode(metadata_value);
+		insertNodeInto(metadata_value_tree_node, parent_metadata_value_tree_node, i);
+		break;
+	    }
+
+	    // Node already exists (don't add again)
+	    if (c == 0) {
+		metadata_value_tree_node = child_metadata_value_tree_node;
+		break;
+	    }
+	}
+
+	// If no node has been added yet, it must go at the end of the list
+	if (metadata_value_tree_node == null) {
+	    metadata_value_tree_node = new MetadataValueTreeNode(metadata_value);
+	    insertNodeInto(metadata_value_tree_node, parent_metadata_value_tree_node, parent_metadata_value_tree_node.getChildCount());
+	}
+
+	// If there is some of the metadata value remaining, add that hierarchically
+	if (!metadata_value_remaining.equals("")) {
+	    metadata_value_tree_node = addMetadataValueInternal(metadata_value_tree_node, metadata_value_remaining);
+	}
+
+	return metadata_value_tree_node;
+    }
+
+
+    public MetadataValueTreeNode getMetadataValueTreeNode(String metadata_value)
+    {
+	return getMetadataValueTreeNodeInternal((MetadataValueTreeNode) root, metadata_value);
+    }
+
+
+    private MetadataValueTreeNode getMetadataValueTreeNodeInternal(MetadataValueTreeNode parent_metadata_value_tree_node, String metadata_value_remaining)
+    {
+	// Split the metadata value into a hierarchy
+	int split_point = metadata_value_remaining.indexOf(MetadataValueTreeNode.METADATA_VALUE_TREE_NODE_HIERARCHY_TOKEN);
+	String metadata_value = ((split_point == -1) ? metadata_value_remaining : metadata_value_remaining.substring(0, split_point));
+	metadata_value_remaining = ((split_point == -1) ? "" : metadata_value_remaining.substring(split_point + 1));
+
+	// Find the value in the tree in the correct place (sorted)
+	MetadataValueTreeNode metadata_value_tree_node = null;
+	for (int i = 0; i < parent_metadata_value_tree_node.getChildCount(); i++) {
+	    MetadataValueTreeNode child_metadata_value_tree_node = (MetadataValueTreeNode) parent_metadata_value_tree_node.getChildAt(i);
+	    int c = metadata_value.compareTo(child_metadata_value_tree_node.getValue());
+
+	    // Node doesn't exist in the tree
+	    if (c < 0) {
+		return null;
+	    }
+	 
+	    // Node exists
+	    if (c == 0) {
+		metadata_value_tree_node = child_metadata_value_tree_node;
+		break;
+	    }
+	}
+
+	// If no node has been found, it doesn't exist
+	if (metadata_value_tree_node == null) {
+	    return null;
+	}
+
+	// If there is some of the metadata value remaining, find that recursively
+	if (!metadata_value_remaining.equals("")) {
+	    metadata_value_tree_node = getMetadataValueTreeNodeInternal(metadata_value_tree_node, metadata_value_remaining);
+	}
+
+	return metadata_value_tree_node;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTreeNode.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTreeNode.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataValueTreeNode.java	(revision 31635)
@@ -0,0 +1,79 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import javax.swing.tree.*;
+
+
+/** This class represents one Metadata value */
+public class MetadataValueTreeNode
+    extends DefaultMutableTreeNode
+{
+    static public final String METADATA_VALUE_TREE_NODE_HIERARCHY_TOKEN = "|";
+
+    private String value = null;
+
+
+    public MetadataValueTreeNode(Object object)
+    {
+	super(object);
+    }
+
+
+    public MetadataValueTreeNode(String value)
+    {
+	this.value = value;
+    }
+
+
+    public String getFullValue()
+    {
+	if (parent == null) {
+	    return "";
+	}
+
+	if (((MetadataValueTreeNode) parent).isRoot()) {
+	    return value;
+	}
+
+	return ((MetadataValueTreeNode) parent).getFullValue() + METADATA_VALUE_TREE_NODE_HIERARCHY_TOKEN + value;
+    }
+
+
+    public String getValue()
+    {
+	return value;
+    }
+
+
+    /** This is used to display the node in the value tree */
+    public String toString()
+    {
+	return value;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataXMLFile.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataXMLFile.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataXMLFile.java	(revision 31635)
@@ -0,0 +1,769 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+
+/** This class represents one metadata.xml file */
+public class MetadataXMLFile
+    extends File
+{
+    static final private String DESCRIPTION_ELEMENT = "Description";
+    static final private String DIRECTORY_FILENAME = ".*";
+    static final private String FILENAME_ELEMENT = "FileName";
+    static final private String FILESET_ELEMENT = "FileSet";
+    static final private String METADATA_ELEMENT = "Metadata";
+	static final private String[] nonEscapingElements = new String[]{FILENAME_ELEMENT};
+	
+    /** Special metadata field: the filename encoding is a unique sort of metadata in
+     * that it is not just information stored with a collection file, but also needs to
+     * be applied in real-time to the collection file (to its filename) for display. */
+    static final public String FILENAME_ENCODING_METADATA = "gs.filenameEncoding";
+	
+    // To speed things up a bit we keep the last accessed metadata.xml file in memory
+    static private File loaded_file = null;
+    static private Document loaded_file_document = null;
+    static private boolean loaded_file_changed = false;
+
+
+    public MetadataXMLFile(String metadata_xml_file_path)
+    {
+	super(metadata_xml_file_path);
+    }
+
+
+    public void addMetadata(CollectionTreeNode file_node, ArrayList metadata_values)
+    {
+	// If this metadata.xml file isn't the one currently loaded, load it now
+	if (loaded_file != this) {
+	    // First we must save out the currently loaded file
+	    saveLoadedFile();
+
+	    // Parse the metadata.xml file
+	    Document document = XMLTools.parseXMLFile(this);
+            if (document == null) {
+		System.err.println("Error: Could not parse metadata.xml file " + getAbsolutePath());
+		return;
+	    }
+
+	    loaded_file = this;
+	    loaded_file_document = document;
+	}
+
+	// Determine the file's path relative to the location of the metadata.xml file
+	String metadata_xml_file_directory_path = FilenameEncoding.fileToURLEncoding(getParentFile());
+	String file_relative_path = file_node.getURLEncodedFilePath().substring(metadata_xml_file_directory_path.length());
+	if (file_relative_path.startsWith(FilenameEncoding.URL_FILE_SEPARATOR)) {
+	    file_relative_path = file_relative_path.substring(FilenameEncoding.URL_FILE_SEPARATOR.length());
+	}
+
+	// Form a regular expression that specifies the scope of the metadata
+	String file_path_regexp;
+	if (file_relative_path.equals("")) {
+	    // Special case for matching all files in the directory
+	    file_path_regexp = DIRECTORY_FILENAME;
+	}
+	else {
+	    // Convert the file path into a regular expression that will match it
+	    file_path_regexp = MetadataTools.getRegularExpressionThatMatchesFilePath(file_relative_path);
+	}
+
+	// Find the appropriate FileSet element for this file
+	Element appropriate_fileset_element = null;
+
+	// Read all the FileSet elements in the file
+	NodeList fileset_elements_nodelist = loaded_file_document.getElementsByTagName(FILESET_ELEMENT);
+	for (int i = 0; i < fileset_elements_nodelist.getLength(); i++) {
+	    Element current_fileset_element = (Element) fileset_elements_nodelist.item(i);
+
+	    // Check the FileName elements of the FileSet to see if we have a match
+	    NodeList filename_elements_nodelist = current_fileset_element.getElementsByTagName(FILENAME_ELEMENT);
+	    for (int j = 0; j < filename_elements_nodelist.getLength(); j++) {
+		Element current_filename_element = (Element) filename_elements_nodelist.item(j);
+		String current_filename_element_value = XMLTools.getElementTextValue(current_filename_element);
+
+		// Only exact matches can be extended with new metadata
+		if (current_filename_element_value.equals(file_path_regexp)) {
+		    appropriate_fileset_element = current_fileset_element;
+		    break;
+		}
+	    }
+	}
+
+	// If no appropriate FileSet element exists create a new one for this file
+	if (appropriate_fileset_element == null) {
+	    DebugStream.println("Creating new FileSet element for file since none exists..."+file_path_regexp);
+	    appropriate_fileset_element = loaded_file_document.createElement(FILESET_ELEMENT);
+
+	    Element new_filename_element = loaded_file_document.createElement(FILENAME_ELEMENT);
+	    new_filename_element.appendChild(loaded_file_document.createTextNode(file_path_regexp));
+	    appropriate_fileset_element.appendChild(new_filename_element);
+
+	    Element new_description_element = loaded_file_document.createElement(DESCRIPTION_ELEMENT);
+	    appropriate_fileset_element.appendChild(new_description_element);
+
+		// add the fileset element for .* at the top: especially important for
+	    // non-accumulating (and override mode) meta. Other type fileset elements can be appended
+	    if(file_path_regexp.equals(DIRECTORY_FILENAME)) {
+			loaded_file_document.getDocumentElement().insertBefore(appropriate_fileset_element,
+					loaded_file_document.getDocumentElement().getFirstChild());
+	    } else {
+	      loaded_file_document.getDocumentElement().appendChild(appropriate_fileset_element);
+	    }
+	}
+
+	// Add each of the metadata values to the FileSet's Description element
+	Element description_element = (Element) appropriate_fileset_element.getElementsByTagName(DESCRIPTION_ELEMENT).item(0);
+	for (int i = 0; i < metadata_values.size(); i++) {
+	    MetadataValue metadata_value = (MetadataValue) metadata_values.get(i);
+	    String metadata_element_name_full = metadata_value.getMetadataElement().getFullName();
+
+	    // Remove any characters that are invalid in XML
+	    String metadata_value_string = XMLTools.removeInvalidCharacters(metadata_value.getFullValue());
+
+	    // Square brackets need to be escaped because they are a special character in Greenstone
+	    metadata_value_string = metadata_value_string.replaceAll("\\[", "&#091;");
+	    metadata_value_string = metadata_value_string.replaceAll("\\]", "&#093;");
+
+	    // the gs.filenameEncoding metadata is unique in that, when added, removed or
+	    // changed, it must be applied on the file(name) whose metadata has been adjusted
+	    if(metadata_element_name_full.equals(FILENAME_ENCODING_METADATA)) {
+			metadata_value_string = processFilenameEncoding(file_path_regexp, 
+										file_node, metadata_value_string, false);
+		                      // true only if removing meta
+	    }
+
+	    // Check if this piece of metadata has already been assigned to this FileSet element
+	    boolean metadata_already_assigned = false;
+	    NodeList metadata_elements_nodelist = description_element.getElementsByTagName(METADATA_ELEMENT);
+	    for (int k = 0; k < metadata_elements_nodelist.getLength(); k++) {
+		Element current_metadata_element = (Element) metadata_elements_nodelist.item(k);
+
+		// Check if the metadata element name matches
+		String current_metadata_element_name_full = current_metadata_element.getAttribute("name");
+		if (current_metadata_element_name_full.equals(metadata_element_name_full)) {
+		  // if the metadata must not accumulate, then edit the current value
+		  if (!metadata_value.isAccumulatingMetadata()) {
+		    XMLTools.setNodeText(current_metadata_element, metadata_value_string);
+		    metadata_already_assigned = true;
+		    break;
+		  }
+		    // Check if the metadata element value matches
+		    String current_metadata_value_string = XMLTools.getElementTextValue(current_metadata_element);
+		    if (current_metadata_value_string.equals(metadata_value_string)) {
+			// Metadata already assigned
+			metadata_already_assigned = true;
+			break;
+		    }
+		}
+	    }
+
+	    // If the piece of metadata hasn't already been assigned, add it now
+	    if (!metadata_already_assigned) {
+		// Create a new Metadata element to record this metadata
+		Element new_metadata_element = loaded_file_document.createElement(METADATA_ELEMENT);
+		new_metadata_element.setAttribute("name", metadata_value.getMetadataElement().getFullName());
+		new_metadata_element.setAttribute("mode", (metadata_value.isAccumulatingMetadata() ? "accumulate" : "override"));
+		new_metadata_element.appendChild(loaded_file_document.createTextNode(metadata_value_string));
+
+		// Accumulating metadata: add at the end
+		if (metadata_value.isAccumulatingMetadata()) {
+		    description_element.appendChild(new_metadata_element);
+		}
+		// Override metadata: add at the start (so it overrides inherited metadata without affecting other assigned metadata)
+		else {
+		    description_element.insertBefore(new_metadata_element, description_element.getFirstChild());
+		}
+	    }
+	}
+
+	// Remember that we've changed the file so it gets saved when a new one is loaded
+	loaded_file_changed = true;
+    }
+
+
+    public ArrayList getMetadataAssignedToFile(File file, boolean fileEncodingOnly)
+    {
+	// If this metadata.xml file isn't the one currently loaded, load it now
+	if (loaded_file != this) {
+	    // First we must save out the currently loaded file
+	    saveLoadedFile();
+
+	    // Parse the metadata.xml file
+	    Document document = XMLTools.parseXMLFile(this);
+              if (document == null) {
+		System.err.println("Error: Could not parse metadata.xml file " + getAbsolutePath());
+		return new ArrayList();
+	    }
+
+	    loaded_file = this;
+	    loaded_file_document = document;
+	}
+
+	// Determine the file's path relative to the location of the metadata.xml file	
+	String file_relative_path = FilenameEncoding.fileToURLEncoding(file);
+	File metadata_xml_file_directory = getParentFile();
+	String metadata_xml_file_directory_path = FilenameEncoding.fileToURLEncoding(metadata_xml_file_directory);
+	file_relative_path = file_relative_path.substring(metadata_xml_file_directory_path.length());
+
+	if (file_relative_path.startsWith(FilenameEncoding.URL_FILE_SEPARATOR)) {
+	    file_relative_path = file_relative_path.substring(FilenameEncoding.URL_FILE_SEPARATOR.length());
+	}
+
+	// Build up a list of metadata assigned to this file
+	ArrayList metadata_values = new ArrayList();
+
+	// Read all the FileSet elements in the file
+	NodeList fileset_elements_nodelist = loaded_file_document.getElementsByTagName(FILESET_ELEMENT);
+	for (int i = 0; i < fileset_elements_nodelist.getLength(); i++) {
+	    Element current_fileset_element = (Element) fileset_elements_nodelist.item(i);
+	    boolean current_fileset_matches = false;
+	    boolean is_one_file_only_metadata = true;
+	    File folder_metadata_inherited_from = null;
+
+	    // Check the FileName elements of the FileSet to see if we have a match
+	    NodeList filename_elements_nodelist = current_fileset_element.getElementsByTagName(FILENAME_ELEMENT);
+	    for (int j = 0; j < filename_elements_nodelist.getLength(); j++) {
+		Element current_filename_element = (Element) filename_elements_nodelist.item(j);
+		String current_filename_element_value = XMLTools.getElementTextValue(current_filename_element);
+
+		// Does this fileset specify metadata for one file only?
+		is_one_file_only_metadata = true;
+		if (current_filename_element_value.indexOf("*") != -1 && !current_filename_element_value.equals(DIRECTORY_FILENAME)) {
+		    // No, it specifies metadata for multiple files (but not all the files in the directory)
+		    is_one_file_only_metadata = false;
+		}
+
+		// This fileset specifies metadata for the file
+		if (file_relative_path.matches(current_filename_element_value)) {
+		    current_fileset_matches = true;
+		    if (!file_relative_path.equals("") && current_filename_element_value.equals(DIRECTORY_FILENAME)) {
+			folder_metadata_inherited_from = metadata_xml_file_directory;
+		    }
+		    break;
+		}
+
+		// This fileset specifies metadata for the folder the file is in
+		if (file_relative_path.startsWith(current_filename_element_value + FilenameEncoding.URL_FILE_SEPARATOR)) {
+		    current_fileset_matches = true;
+		    folder_metadata_inherited_from = new File(metadata_xml_file_directory, current_filename_element_value);
+		    break;
+		}
+	    }
+
+	    // The FileSet doesn't apply, so move onto the next one
+	    if (current_fileset_matches == false) {
+		continue;
+	    }
+
+	    // Read all the Metadata elements in the fileset
+	    NodeList metadata_elements_nodelist = current_fileset_element.getElementsByTagName(METADATA_ELEMENT);
+	    for (int k = 0; k < metadata_elements_nodelist.getLength(); k++) {
+		Element current_metadata_element = (Element) metadata_elements_nodelist.item(k);
+		String metadata_element_name_full = current_metadata_element.getAttribute("name");
+		// if we're only looking for fileEncoding metadata and this isn't it, skip to the next
+		if(fileEncodingOnly && !metadata_element_name_full.equals(FILENAME_ENCODING_METADATA)) {
+		    continue;
+		}		
+		String metadata_set_namespace = MetadataTools.getMetadataSetNamespace(metadata_element_name_full);
+
+		// Ignore legacy crap
+		if (metadata_set_namespace.equals("hidden")) {
+		    continue;
+		}
+
+		MetadataSet metadata_set = MetadataSetManager.getMetadataSet(metadata_set_namespace);
+		if (metadata_set == null) {
+		    // The metadata set isn't loaded, so give the option of mapping the element into a loaded set
+		    String target_metadata_element_name_full = MetadataSetManager.mapUnloadedMetadataElement(metadata_element_name_full);
+		    if (target_metadata_element_name_full == null || target_metadata_element_name_full.equals("")) {
+			// Skip this element if we still don't have a loaded element for it
+			continue;
+		    }
+
+		    metadata_element_name_full = target_metadata_element_name_full;
+		    metadata_set_namespace = MetadataTools.getMetadataSetNamespace(metadata_element_name_full);
+		    metadata_set = MetadataSetManager.getMetadataSet(metadata_set_namespace);
+		}
+
+		MetadataElement metadata_element = MetadataTools.getMetadataElementWithName(metadata_element_name_full);
+
+                String metadata_element_name = MetadataTools.getMetadataElementName(metadata_element_name_full);
+		// If the element doesn't exist in the metadata set, we're not interested
+                //Shaoqun modified. It needs to be added to metadata_set because the user might disable skim file   
+		if (metadata_element == null) {
+		    metadata_element = metadata_set.addMetadataElementForThisSession(metadata_element_name);
+		    // continue;
+		}
+
+		// Square brackets need to be escaped because they are a special character in Greenstone
+		String metadata_value_string = XMLTools.getElementTextValue(current_metadata_element);
+		metadata_value_string = metadata_value_string.replaceAll("&#091;", "[");
+		metadata_value_string = metadata_value_string.replaceAll("&#093;", "]");
+
+		MetadataValueTreeNode metadata_value_tree_node = metadata_element.getMetadataValueTreeNode(metadata_value_string);
+
+		// If there is no metadata value tree node for this value, create it
+		if (metadata_value_tree_node == null) {
+		    DebugStream.println("Note: No value tree node for metadata value \"" + metadata_value_string + "\"");
+		    metadata_element.addMetadataValue(metadata_value_string);
+		    metadata_value_tree_node = metadata_element.getMetadataValueTreeNode(metadata_value_string);
+		}
+
+		MetadataValue metadata_value = new MetadataValue(metadata_element, metadata_value_tree_node);
+		metadata_value.inheritsMetadataFromFolder(folder_metadata_inherited_from);
+		metadata_value.setIsOneFileOnlyMetadata(is_one_file_only_metadata);
+
+		// Is this accumulating metadata?
+		if (current_metadata_element.getAttribute("mode").equals("accumulate")) {
+		    metadata_value.setIsAccumulatingMetadata(true);
+		}
+
+		// Add the new metadata value to the list
+		metadata_values.add(metadata_value);
+	    }
+	}
+
+	return metadata_values;
+    }
+
+
+    public void removeMetadata(CollectionTreeNode file_node, ArrayList metadata_values)
+    {
+	// If this metadata.xml file isn't the one currently loaded, load it now
+	if (loaded_file != this) {
+	    // First we must save out the currently loaded file
+	    saveLoadedFile();
+
+	    // Parse the metadata.xml file
+	    Document document = XMLTools.parseXMLFile(this);
+	    if (document == null) {
+		System.err.println("Error: Could not parse metadata.xml file " + getAbsolutePath());
+		return;
+	    }
+
+	    loaded_file = this;
+	    loaded_file_document = document;
+	}
+
+	// Determine the file's path relative to the location of the metadata.xml file
+	String metadata_xml_file_directory_path = FilenameEncoding.fileToURLEncoding(getParentFile());
+	String file_relative_path = file_node.getURLEncodedFilePath().substring(metadata_xml_file_directory_path.length());
+	if (file_relative_path.startsWith(FilenameEncoding.URL_FILE_SEPARATOR)) {
+	    file_relative_path = file_relative_path.substring(FilenameEncoding.URL_FILE_SEPARATOR.length());
+	}
+
+	// Form a regular expression that specifies the scope of the metadata
+	String file_path_regexp;
+	if (file_relative_path.equals("")) {
+	    // Special case for matching all files in the directory
+	    file_path_regexp = DIRECTORY_FILENAME;
+	}
+	else {
+	    // Convert the file path into a regular expression that will match it
+	    file_path_regexp = MetadataTools.getRegularExpressionThatMatchesFilePath(file_relative_path);
+	}
+
+	// Find the appropriate FileSet element for this file
+	Element appropriate_fileset_element = null;
+
+	// Read all the FileSet elements in the file
+	NodeList fileset_elements_nodelist = loaded_file_document.getElementsByTagName(FILESET_ELEMENT);
+	for (int i = 0; i < fileset_elements_nodelist.getLength(); i++) {
+	    Element current_fileset_element = (Element) fileset_elements_nodelist.item(i);
+
+	    // Check the FileName elements of the FileSet to see if we have a match
+	    NodeList filename_elements_nodelist = current_fileset_element.getElementsByTagName(FILENAME_ELEMENT);
+	    for (int j = 0; j < filename_elements_nodelist.getLength(); j++) {
+		Element current_filename_element = (Element) filename_elements_nodelist.item(j);
+		String current_filename_element_value = XMLTools.getElementTextValue(current_filename_element);
+
+		// Only exact matches can be extended with new metadata
+		if (current_filename_element_value.equals(file_path_regexp)) {
+		    appropriate_fileset_element = current_fileset_element;
+		    break;
+		}
+	    }
+	}
+
+	// If no appropriate FileSet element exists the metadata isn't assigned in this metadata.xml file
+	if (appropriate_fileset_element == null) {
+	    DebugStream.println("Note: No appropriate FileSet element found when removing metadata from " + this);
+	    return;
+	}
+
+	// Remove each of the metadata values from the FileSet's Description element
+	for (int i = 0; i < metadata_values.size(); i++) {
+	    MetadataValue metadata_value = (MetadataValue) metadata_values.get(i);
+
+	    // Remove any characters that are invalid in XML
+	    String metadata_value_string = XMLTools.removeInvalidCharacters(metadata_value.getFullValue());
+
+	    // Square brackets need to be escaped because they are a special character in Greenstone
+	    metadata_value_string = metadata_value_string.replaceAll("\\[", "&#091;");
+	    metadata_value_string = metadata_value_string.replaceAll("\\]", "&#093;");
+
+	    // Find the Metadata element to delete from the fileset
+	    String metadata_element_name_full = metadata_value.getMetadataElement().getFullName();
+	    NodeList metadata_elements_nodelist = appropriate_fileset_element.getElementsByTagName(METADATA_ELEMENT);
+	    for (int k = 0; k < metadata_elements_nodelist.getLength(); k++) {
+		Element current_metadata_element = (Element) metadata_elements_nodelist.item(k);
+
+		// Check the metadata element name matches
+		String current_metadata_element_name_full = current_metadata_element.getAttribute("name");
+		if (current_metadata_element_name_full.equals(metadata_element_name_full)) {
+		    // Check the metadata element value matches
+		    String current_metadata_value_string = XMLTools.getElementTextValue(current_metadata_element);
+		    if (current_metadata_value_string.equals(metadata_value_string)) {
+			
+			// Remove this Metadata element
+			current_metadata_element.getParentNode().removeChild(current_metadata_element);
+			
+			// the gs.filenameEncoding metadata is unique in that, when added, removed or
+			// changed, it must be applied on the file(name) whose metadata has been adjusted
+			if(current_metadata_element_name_full.equals(FILENAME_ENCODING_METADATA)) {
+			
+				// metadata_value_string will hereafter be the inherited gs.FilenameEncoding 
+				// metadata (if any), now that the value at this level has been removed
+			    metadata_value_string = processFilenameEncoding(file_path_regexp,
+						file_node, "", true); // true only if *removing* this meta	
+			}
+			
+			// If there are no Metadata elements left now, remove the (empty) FileSet element
+			if (metadata_elements_nodelist.getLength() == 0) {
+			    appropriate_fileset_element.getParentNode().removeChild(appropriate_fileset_element);
+			}
+
+			break;
+		    }
+		}
+	    }
+	}
+
+	// Remember that we've changed the file so it gets saved when a new one is loaded
+	loaded_file_changed = true;
+    }
+
+
+    public void replaceMetadata(CollectionTreeNode file_node, MetadataValue old_metadata_value, MetadataValue new_metadata_value)
+    {
+	// If this metadata.xml file isn't the one currently loaded, load it now
+	if (loaded_file != this) {
+	    // First we must save out the currently loaded file
+	    saveLoadedFile();
+
+	    // Parse the metadata.xml file
+	    Document document = XMLTools.parseXMLFile(this);
+	    if (document == null) {
+		System.err.println("Error: Could not parse metadata.xml file " + getAbsolutePath());
+		return;
+	    }
+
+	    loaded_file = this;
+	    loaded_file_document = document;
+	}
+
+	// Determine the file's path relative to the location of the metadata.xml file
+	String metadata_xml_file_directory_path = FilenameEncoding.fileToURLEncoding(getParentFile());
+	String file_relative_path = file_node.getURLEncodedFilePath().substring(metadata_xml_file_directory_path.length());
+	if (file_relative_path.startsWith(FilenameEncoding.URL_FILE_SEPARATOR)) {
+	    file_relative_path = file_relative_path.substring(FilenameEncoding.URL_FILE_SEPARATOR.length());
+	}
+
+	// Form a regular expression that specifies the scope of the metadata
+	String file_path_regexp;
+	if (file_relative_path.equals("")) {
+	    // Special case for matching all files in the directory
+	    file_path_regexp = DIRECTORY_FILENAME;
+	}
+	else {
+	    // Convert the file path into a regular expression that will match it
+	    file_path_regexp = MetadataTools.getRegularExpressionThatMatchesFilePath(file_relative_path);
+	}
+
+	// Remove any characters that are invalid in XML
+	String old_metadata_value_string = XMLTools.removeInvalidCharacters(old_metadata_value.getFullValue());
+	String new_metadata_value_string = XMLTools.removeInvalidCharacters(new_metadata_value.getFullValue());
+
+	// Square brackets need to be escaped because they are a special character in Greenstone
+	old_metadata_value_string = old_metadata_value_string.replaceAll("\\[", "&#091;");
+	old_metadata_value_string = old_metadata_value_string.replaceAll("\\]", "&#093;");
+	new_metadata_value_string = new_metadata_value_string.replaceAll("\\[", "&#091;");
+	new_metadata_value_string = new_metadata_value_string.replaceAll("\\]", "&#093;");
+
+	// Read all the FileSet elements in the file
+	NodeList fileset_elements_nodelist = loaded_file_document.getElementsByTagName(FILESET_ELEMENT);
+	for (int i = 0; i < fileset_elements_nodelist.getLength(); i++) {
+	    Element current_fileset_element = (Element) fileset_elements_nodelist.item(i);
+	    boolean current_fileset_matches = false;
+
+	    // Check the FileName elements of the FileSet to see if we have a match
+	    NodeList filename_elements_nodelist = current_fileset_element.getElementsByTagName(FILENAME_ELEMENT);
+	    for (int j = 0; j < filename_elements_nodelist.getLength(); j++) {
+		Element current_filename_element = (Element) filename_elements_nodelist.item(j);
+		String current_filename_element_value = XMLTools.getElementTextValue(current_filename_element);
+
+		// Only exact matches can be edited
+		if (current_filename_element_value.equals(file_path_regexp)) {
+		    current_fileset_matches = true;
+		    break;
+		}
+	    }
+
+	    // The FileSet doesn't apply, so move onto the next one
+	    if (current_fileset_matches == false) {
+		continue;
+	    }
+
+	    // Each metadata value is only allowed to be assigned once
+	    boolean new_metadata_value_already_exists = false;
+	    Element metadata_element_to_edit = null;
+
+	    // Find the Metadata element to replace in the fileset
+	    String metadata_element_name_full = old_metadata_value.getMetadataElement().getFullName();
+	    NodeList metadata_elements_nodelist = current_fileset_element.getElementsByTagName(METADATA_ELEMENT);
+	    for (int k = 0; k < metadata_elements_nodelist.getLength(); k++) {
+		Element current_metadata_element = (Element) metadata_elements_nodelist.item(k);
+
+		// Check the metadata element name matches
+		String current_metadata_element_name_full = current_metadata_element.getAttribute("name");
+		if (!current_metadata_element_name_full.equals(metadata_element_name_full)) {
+		    continue;
+		}
+
+		// Check the new metadata value doesn't already exist
+		String current_metadata_value_string = XMLTools.getElementTextValue(current_metadata_element);
+		if (current_metadata_value_string.equals(new_metadata_value_string)) {
+		    new_metadata_value_already_exists = true;
+		}
+
+		// Check the metadata element value matches
+		if (current_metadata_value_string.equals(old_metadata_value_string)) {
+		    metadata_element_to_edit = current_metadata_element;
+		}
+	    }
+
+	    // If the new metadata value already existed, remove the original value
+	    if (new_metadata_value_already_exists) {
+			if(metadata_element_to_edit != null) { //?????????
+			    metadata_element_to_edit.getParentNode().removeChild(metadata_element_to_edit);
+			} else {
+			    System.err.println("ERROR MetadataXMLFile: metadata_element_to_edit is null");
+			}
+	    }
+	    // Otherwise replace the old value with the new value
+	    // Ensure metadata_element_to_edit isn't null (may occur when multiple files are selected)
+	    else if (metadata_element_to_edit != null) {
+		
+		// the gs.filenameEncoding metadata is unique in that, when added, removed or
+		// changed, it must be applied on the file(name) whose metadata has been adjusted
+		if(metadata_element_name_full.equals(FILENAME_ENCODING_METADATA)) {
+		    new_metadata_value_string = processFilenameEncoding(file_path_regexp, file_node, new_metadata_value_string, false);
+		    // true only if removing meta		
+		}		
+		XMLTools.setElementTextValue(metadata_element_to_edit, new_metadata_value_string);
+	    }
+	}
+
+	// Remember that we've changed the file so it gets saved when a new one is loaded
+	loaded_file_changed = true;
+    }
+
+
+    static public void saveLoadedFile()
+    {
+	// If we have a file loaded into memory and it has been modified, save it now
+	if (loaded_file != null && loaded_file_changed == true) {
+	    XMLTools.writeXMLFile(loaded_file, loaded_file_document, nonEscapingElements);
+	
+		
+	    loaded_file_changed = false;
+	}
+    }
+
+
+    /**
+     * Every metadata.xml file must be skimmed when a collection is opened, for three very important reasons:
+     *   - To handle any non-namespaced metadata in the metadata.xml files (this is mapped and the files rewritten)
+     *   - To get a complete list of the metadata elements in the collection (used in Design and Format panes)
+     *   - To build complete and accurate metadata value trees (used in the Enrich pane)
+     */
+    public void skimFile()
+    {
+	boolean file_changed = false;
+
+	// Parse the metadata.xml file
+	DebugStream.println("Skimming metadata.xml file " + this + "...");
+	       
+	Document document = XMLTools.parseXMLFile(this);
+	if (document == null) {
+	    System.err.println("Error: Could not parse metadata.xml file " + getAbsolutePath());
+	    return;
+	}
+
+	// Read all the Metadata elements in the file
+	HashMap target_metadata_element_name_attrs_cache = new HashMap();
+	NodeList metadata_elements_nodelist = document.getElementsByTagName(METADATA_ELEMENT);
+	for (int i = 0; i < metadata_elements_nodelist.getLength(); i++) {
+	    Element current_metadata_element = (Element) metadata_elements_nodelist.item(i);
+	    String metadata_element_name_full = current_metadata_element.getAttribute("name");
+	    String metadata_set_namespace = MetadataTools.getMetadataSetNamespace(metadata_element_name_full);
+
+	    // Ignore legacy crap
+	    if (metadata_set_namespace.equals("hidden")) {
+		continue;
+	    }
+
+	    MetadataSet metadata_set = MetadataSetManager.getMetadataSet(metadata_set_namespace);
+	    if (metadata_set == null) {
+		// The metadata set isn't loaded, so give the option of mapping the element into a loaded set
+		String target_metadata_element_name_full = MetadataSetManager.mapUnloadedMetadataElement(metadata_element_name_full);
+		if (target_metadata_element_name_full == null || target_metadata_element_name_full.equals("")) {
+		    // Skip this element if we still don't have a loaded element for it
+		    continue;
+		}
+
+		// Update the metadata.xml file to have the new (namespaced) element name
+		// Instead of using current_metadata_element.setAttribute("name", target_metadata_element_name_full)
+		//   we create an Attr object for each target metadata element name, and cache them
+		// This makes a *huge* difference (namespacing a metadata.xml file with 45000 metadata entries now
+		//   takes 45 seconds instead of 30 minutes!) -- why is setting the value of a Node so slow?
+		Attr target_metadata_element_name_attr = (Attr) target_metadata_element_name_attrs_cache.get(target_metadata_element_name_full);
+		if (target_metadata_element_name_attr == null) {
+		    target_metadata_element_name_attr = document.createAttribute("name");
+		    target_metadata_element_name_attr.setValue(target_metadata_element_name_full);
+		    target_metadata_element_name_attrs_cache.put(target_metadata_element_name_full, target_metadata_element_name_attr);
+		}
+
+		// Remove the old name attribute and add the new (namespaced) one
+		current_metadata_element.removeAttribute("name");
+		current_metadata_element.setAttributeNode((Attr) target_metadata_element_name_attr.cloneNode(false));
+		file_changed = true;
+
+		metadata_element_name_full = target_metadata_element_name_full;
+		metadata_set_namespace = MetadataTools.getMetadataSetNamespace(metadata_element_name_full);
+		metadata_set = MetadataSetManager.getMetadataSet(metadata_set_namespace);
+	    }
+
+	    String metadata_element_name = MetadataTools.getMetadataElementName(metadata_element_name_full);
+	    MetadataElement metadata_element = metadata_set.getMetadataElementWithName(metadata_element_name);
+
+	    // If the element doesn't exist in the metadata set, add it
+	    if (metadata_element == null) {
+		metadata_element = metadata_set.addMetadataElementForThisSession(metadata_element_name);
+	    }
+
+	    // Square brackets need to be escaped because they are a special character in Greenstone
+	    String metadata_value_string = XMLTools.getElementTextValue(current_metadata_element);
+	    metadata_value_string = metadata_value_string.replaceAll("&#091;", "[");
+	    metadata_value_string = metadata_value_string.replaceAll("&#093;", "]");
+
+	    metadata_element.addMetadataValue(metadata_value_string);
+	}
+
+	// Rewrite the metadata.xml file if it has changed
+	if (file_changed) {
+	    XMLTools.writeXMLFile(this, document);
+	}
+    }
+
+	/** 
+	 * The gs.filenameEncoding metadata is unique in that, when added, removed or
+	 * replaced, it must be applied on the file(name) whose metadata has been 
+	 * adjusted. 
+	 * This method handles all that, given the regular expression or filepath name 
+	 * to match on (.* matches subdirectories), the affected fileNode, the new 
+	 * encoding value and whether a new encoding value has been added/an existing
+	 * one has been replaced or whether the encoding metadata has been removed.
+	 * The new adjusted value for the encoding metadata is returned.
+	 *
+	 * MetadataXMLFileManager maintains a hashmap of (URL-encoded filepaths, encoding) 
+	 * to allow fast access to previously assigned gs.filenameEncoding metadata (if
+	 * any) for each file. This hashmap also needs to be updated, but this update
+	 * is complicated by the fact that it concerns regular expressions that could
+	 * affect multiple filenames.
+	 */
+    public String processFilenameEncoding(String file_path_regexp, CollectionTreeNode file_node, 
+					String encoding_metadata_value, boolean removingMetadata) 
+    {
+		if(!FilenameEncoding.MULTIPLE_FILENAME_ENCODINGS_SUPPORTED) {
+		    return encoding_metadata_value;
+		}
+
+		// Work out this filenode's new encoding and apply it:
+		
+		if(removingMetadata) { // encoding_metadata_value = ""
+			// gs.filenameEncoding metadata being removed, work out 
+			// any inherited metadata to replace it with in the meta-table
+			encoding_metadata_value = FilenameEncoding.getInheritedFilenameEncoding(
+						file_node.getURLEncodedFilePath(), file_node.getFile());
+			// should be canonical encoding already
+		} 
+		else if(!encoding_metadata_value.equals("")) { 
+			// if adding or replacing filename encoding,
+			// get the canonical encoding name for this alias
+			encoding_metadata_value = FilenameEncoding.canonicalEncodingName(encoding_metadata_value);
+		}	
+		// Reencode the display of this filenode only as any affected 
+		// childnodes will be reencoded on FileNode.refreshDescendantEncodings()
+		file_node.reencodeDisplayName(encoding_metadata_value);
+			
+
+		// Whether removing or adding/replacing the file's gs.filename encoding meta,
+		// store this in the file-to-encoding map for fast access, since the map stores
+		// empty string values when no meta has been assigned at this file level.
+		// In the case of removingMetadata, the value stored will be the fallback value
+		
+		String urlpath = file_node.getURLEncodedFilePath();
+		if(removingMetadata) { 
+			// remove it from the map instead of inserting "", so that when folders in the collectiontree 
+			// are being deleted or shifted, the removemetada (and addmetadata) calls that get fired 
+			// for each affected filenodes does not cause the undesirable effect of multiple "" to be 
+			// entered into the filename-to-encoding map for filepaths that no longer exist .
+			FilenameEncoding.map.remove(urlpath); 
+		} else { // for adding and replacing, put the encoding into the map (also replaces any existing encoding for it)
+			FilenameEncoding.map.put(urlpath, encoding_metadata_value);			
+		}
+		
+		// If new folder-level metadata (or metadata for a set of files fitting a pattern) has been
+		//  assigned, the file_to_encodings map will be cleared for all descendant folders and files,
+		//  so that these can be re-calculated upon refreshing the visible parts of the CollectionTree. 
+		// Mark the state as requiring a refresh of the CollectionTree. 
+		// This next step also serves to prevent the MetadataValueTableModel from trying to update
+		// itself while a refresh (involving re-encoding of filenames of visible nodes) is in progress.
+		FilenameEncoding.setRefreshRequired(true);
+		
+		return encoding_metadata_value;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataXMLFileManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataXMLFileManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/MetadataXMLFileManager.java	(revision 31635)
@@ -0,0 +1,489 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.XMLTools;
+
+
+/** This class is a static class that manages the metadata.xml files */
+public class MetadataXMLFileManager
+{
+    static private ArrayList metadata_xml_files = new ArrayList();
+    /** The objects listening for MetadataChanged events. */
+    static private ArrayList metadata_changed_listeners = new ArrayList();
+    /** Keep track of which metadata.xml files have been modified so we can upload them to the server */
+    static private ArrayList modified_metadata_xml_files = new ArrayList();
+
+  /** For non-accumulating metadata (gs.FilenameEncoding), need the metadata.xml files
+   * sorted in top-down folder level order, which is achieved through this Comparator. 
+   * By declaring a static class member here, we avoid recreating this object for every 
+   * comparison, which would otherwise have resulted in a constructor call each time. */
+  static private MetadataXMLFileComparator metadataXMLFileComparator = new MetadataXMLFileComparator();
+
+
+    static public void addMetadata(CollectionTreeNode file_node, ArrayList metadata_values)
+    {
+	addMetadata(new CollectionTreeNode[] { file_node }, metadata_values);
+    }
+
+
+    static public void addMetadata(CollectionTreeNode[] file_nodes, MetadataValue metadata_value)
+    {
+	ArrayList metadata_values = new ArrayList();
+	metadata_values.add(metadata_value);
+	addMetadata(file_nodes, metadata_values);
+    }
+
+
+    static public void addMetadata(CollectionTreeNode[] file_nodes, ArrayList metadata_values)
+    {
+	// Check the list of metadata values is non-empty
+	if (metadata_values.isEmpty()) {
+	    return;
+	}
+
+	// Add the metadata to each file node in turn
+	for (int i = 0; i < file_nodes.length; i++) {
+	    File current_file = file_nodes[i].getFile();
+	    DebugStream.println("Adding metadata to " + current_file.getAbsolutePath());
+
+	    // Find which metadata.xml file needs editing
+	    boolean applicable_metadata_xml_file_found = false;
+	    File current_file_directory = (current_file.isDirectory() ? current_file : current_file.getParentFile());
+	    String current_file_directory_path = current_file_directory.getAbsolutePath();
+	    for (int j = 0; j < metadata_xml_files.size(); j++) {
+		MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(j);
+
+		// This metadata.xml file is only applicable if it is at the same level as the file
+		if (current_file_directory_path.equals(metadata_xml_file.getParentFile().getAbsolutePath())) {
+		    applicable_metadata_xml_file_found = true;
+		    metadata_xml_file.addMetadata(file_nodes[i], metadata_values);
+		    if (!modified_metadata_xml_files.contains(metadata_xml_file)) {
+			modified_metadata_xml_files.add(metadata_xml_file);
+		    }
+		}
+	    }
+
+	    // If no applicable metadata.xml file exists, we have to create a new one
+	    if (!applicable_metadata_xml_file_found) {
+		// Create a new (empty) metadata.xml file in the file's directory...
+		File new_metadata_xml_file_file = new File(current_file_directory, "metadata.xml");
+		XMLTools.writeXMLFile(new_metadata_xml_file_file, XMLTools.parseXMLFile("xml/metadata.xml", true));
+
+		// ...load it...
+		MetadataXMLFile new_metadata_xml_file = loadMetadataXMLFile(new_metadata_xml_file_file,true);
+
+		// ...and add the metadata
+		new_metadata_xml_file.addMetadata(file_nodes[i], metadata_values);
+		if (!modified_metadata_xml_files.contains(new_metadata_xml_file)) {
+		    modified_metadata_xml_files.add(new_metadata_xml_file);
+		}
+	    }
+	}
+
+	// Let any listeners know that the metadata has changed
+	fireMetadataChangedEvent(file_nodes);
+    }
+
+
+    static public void addMetadataChangedListener(MetadataChangedListener metadata_changed_listener)
+    {
+	metadata_changed_listeners.add(metadata_changed_listener);
+    }
+
+
+    static public void clearMetadataXMLFiles()
+    {
+	metadata_xml_files.clear();
+    }
+
+
+    static private void fireMetadataChangedEvent(CollectionTreeNode[] file_nodes)
+    {
+	// Send the event off to all the MetadataChangedListeners
+	for (int i = 0; i < metadata_changed_listeners.size(); i++) {
+	    ((MetadataChangedListener) metadata_changed_listeners.get(i)).metadataChanged(file_nodes);
+	}
+    }
+
+
+    /** Returns the metadata assigned to a file outside the collection, excluding folder-level/inherited metadata. */
+    static public ArrayList getMetadataAssignedDirectlyToExternalFile(File file)
+    {
+	DebugStream.println("Getting metadata assigned directly to external file " + file + "...");
+
+	// Build up a list of applicable metadata.xml files
+	ArrayList applicable_metadata_xml_files = new ArrayList();
+
+	File directory = (file.isDirectory() ? file : file.getParentFile());
+	while (directory != null) {
+	    File metadata_xml_file = new File(directory, "metadata.xml");
+	    if (metadata_xml_file.exists() && !metadata_xml_file.isDirectory()) {
+		// It is very important that shallower files come before deeper ones
+		applicable_metadata_xml_files.add(0, new MetadataXMLFile(metadata_xml_file.getAbsolutePath()));
+	    }
+
+	    directory = directory.getParentFile();
+	}
+
+	// Get the metadata assigned to the specified file from the applicable metadata.xml files
+	ArrayList assigned_metadata = getMetadataAssignedToFile(file, applicable_metadata_xml_files, false);
+
+	// Remove any folder-level metadata
+	for (int i = assigned_metadata.size() - 1; i >= 0; i--) {
+	    if (((MetadataValue) assigned_metadata.get(i)).isInheritedMetadata()) {
+		assigned_metadata.remove(i);
+	    }
+	}
+
+	return assigned_metadata;
+    }
+
+
+	/** Returns the metadata assigned to a file inside the collection, excluding folder-level/inherited metadata. */
+    static public ArrayList getMetadataAssignedDirectlyToFile(File file)
+    {
+		return getMetadataAssignedDirectlyToFile(file, false);
+    }
+	
+    /** Returns the metadata assigned to a file inside the collection, excluding folder-level/inherited metadata. */
+    static public ArrayList getMetadataAssignedDirectlyToFile(File file, boolean filenameEncodingMetaOnly)
+    {
+		
+		// Get all the metadata assigned to the specified file...
+		ArrayList assigned_metadata = getMetadataAssignedToFile(file, filenameEncodingMetaOnly);
+
+		// ...then remove any folder-level metadata
+		for (int i = assigned_metadata.size() - 1; i >= 0; i--) {
+		    if (((MetadataValue) assigned_metadata.get(i)).isInheritedMetadata()) {
+			assigned_metadata.remove(i);
+		    }
+		}
+
+		return assigned_metadata;
+		/*
+		// Get all the metadata assigned to the specified file...
+		// Build up a list of applicable metadata.xml files - which in this case 
+		// is exclusively the metadata file at this file/folder's own level.
+		ArrayList applicable_metadata_xml_files = new ArrayList();
+
+		// Find the metadata.xml file (if any) that is at the same level as the file
+		String file_directory_path = (file.isDirectory() ? file : file.getParentFile()).getAbsolutePath() + File.separator;
+		for (int i = 0; i < metadata_xml_files.size(); i++) {
+		    MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(i);
+		    
+		    if (file_directory_path.equals(metadata_xml_file.getParentFile().getAbsolutePath() + File.separator)) {
+				//System.err.println("Found metadata_xml_file: " + metadata_xml_file);
+				applicable_metadata_xml_files.add(metadata_xml_file);
+		    }
+		}
+		
+		if(applicable_metadata_xml_files.size() == 0) {
+		    return new ArrayList(0);
+		}
+
+		// Return the metadata assigned to the specified file from the applicable metadata.xml files
+		return getMetadataAssignedToFile(file, applicable_metadata_xml_files, filenameEncodingMetaOnly);
+		*/
+    }
+	
+
+    /** Returns all the metadata assigned to a file inside the collection. */
+    static public ArrayList getMetadataAssignedToFile(File file)
+    {
+	return getMetadataAssignedToFile(file, false);
+    }
+	
+	/** Returns all the metadata assigned to a file inside the collection (or 
+	 *	just gs.filenameEncoding if parameter filenameEncodingMetaOnly is true),
+	 * 	including folder-level/inherited metadata.
+	 */    
+    static public ArrayList getMetadataAssignedToFile(File file, boolean filenameEncodingMetaOnly)
+    {
+	// Build up a list of applicable metadata.xml files
+	ArrayList applicable_metadata_xml_files = new ArrayList();
+
+	// Look at each loaded metadata.xml file to see if it is potentially applicable
+	String file_directory_path = (file.isDirectory() ? file : file.getParentFile()).getAbsolutePath() + File.separator;
+	for (int i = 0; i < metadata_xml_files.size(); i++) {
+	    MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(i);
+
+	    // This metadata.xml file is only potentially applicable if it is above or at the same level as the file
+	    if (file_directory_path.startsWith(metadata_xml_file.getParentFile().getAbsolutePath() + File.separator)) {
+		applicable_metadata_xml_files.add(metadata_xml_file);
+	    }
+	}
+
+	// Sort the metadataxml files in order starting from those in the 
+	// topmost folders down to the one in the lowest level folder.
+	Collections.sort(applicable_metadata_xml_files, metadataXMLFileComparator);
+	// Return the metadata assigned to the specified file from the applicable metadata.xml files
+	return getMetadataAssignedToFile(file, applicable_metadata_xml_files, filenameEncodingMetaOnly);
+    }
+
+	// package access method
+    static ArrayList getMetadataAssignedToFile(File file, ArrayList applicable_metadata_xml_files,
+						       boolean filenameEncodingMetaOnly)	
+    {
+	// Build up a list of metadata values assigned to this file
+	ArrayList metadata_values_all = new ArrayList();
+
+	// Look at each applicable metadata.xml file to see if it assigns metadata to this file
+	for (int i = 0; i < applicable_metadata_xml_files.size(); i++) {
+	    MetadataXMLFile metadata_xml_file = (MetadataXMLFile) applicable_metadata_xml_files.get(i);
+	    DebugStream.println("Applicable metadata.xml file: " + metadata_xml_file);
+
+	    ArrayList metadata_values = metadata_xml_file.getMetadataAssignedToFile(file, filenameEncodingMetaOnly);
+	    for (int j = 0; j < metadata_values.size(); j++) {
+		MetadataValue metadata_value = (MetadataValue) metadata_values.get(j);
+
+		// Overriding metadata: remove any values with this metadata element
+		if (metadata_value.isAccumulatingMetadata() == false) {
+		    for (int k = metadata_values_all.size() - 1; k >= 0; k--) {
+			if (((MetadataValue) metadata_values_all.get(k)).getMetadataElement().equals(metadata_value.getMetadataElement())) {
+			    metadata_values_all.remove(k);
+			}
+		    }
+		}
+
+		metadata_values_all.add(metadata_value);
+	    }
+	}
+
+	return metadata_values_all;
+    }
+
+
+    static public void loadMetadataXMLFiles(File directory, boolean skimfile)
+    {
+	// Make sure the directory (import) exists
+	if (directory.exists() == false) {
+	    return;
+	}
+
+	// Look recursively at each subfile of the directory for metadata.xml files
+	File[] directory_files = directory.listFiles();
+	for (int i = 0; i < directory_files.length; i++) {
+	    File child_file = directory_files[i];
+	    if (child_file.isDirectory()) {
+		loadMetadataXMLFiles(child_file,skimfile);
+	    }
+	    else if (child_file.getName().equals("metadata.xml")) {
+		loadMetadataXMLFile(child_file,skimfile);
+	    }
+	}
+    }
+
+
+    static private MetadataXMLFile loadMetadataXMLFile(File metadata_xml_file_file, boolean skimfile)
+    {
+	MetadataXMLFile metadata_xml_file = new MetadataXMLFile(metadata_xml_file_file.getAbsolutePath());
+	if (metadata_xml_files.contains(metadata_xml_file)) {
+	    // This metadata.xml file has already been loaded, so return the loaded object
+	    return (MetadataXMLFile) metadata_xml_files.get(metadata_xml_files.indexOf(metadata_xml_file));
+	}
+	if(skimfile){
+	   metadata_xml_file.skimFile();
+	}
+	metadata_xml_files.add(metadata_xml_file);
+	return metadata_xml_file;
+    }
+
+
+    static public void removeMetadata(CollectionTreeNode file_node, ArrayList metadata_values)
+    {
+	removeMetadata(new CollectionTreeNode[] { file_node }, metadata_values);
+    }
+
+
+    static public void removeMetadata(CollectionTreeNode[] file_nodes, MetadataValue metadata_value)
+    {
+	ArrayList metadata_values = new ArrayList();
+	metadata_values.add(metadata_value);
+	removeMetadata(file_nodes, metadata_values);
+    }
+
+
+    static public void removeMetadata(CollectionTreeNode[] file_nodes, ArrayList metadata_values)
+    {
+	// Check the list of metadata values is non-empty
+	if (metadata_values.isEmpty()) {
+	    return;
+	}
+
+	// Remove the metadata from each file node in turn
+	for (int i = 0; i < file_nodes.length; i++) {
+	    File current_file = file_nodes[i].getFile();
+	    DebugStream.println("Removing metadata from " + current_file.getAbsolutePath());
+
+	    // Find which metadata.xml file needs editing
+	    File current_file_directory = (current_file.isDirectory() ? current_file : current_file.getParentFile());
+	    String current_file_directory_path = current_file_directory.getAbsolutePath();
+	    for (int j = 0; j < metadata_xml_files.size(); j++) {
+		MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(j);
+
+		// This metadata.xml file is only potentially applicable if it is above or at the same level as the file
+		if (current_file_directory_path.startsWith(metadata_xml_file.getParentFile().getAbsolutePath())) {
+		    metadata_xml_file.removeMetadata(file_nodes[i], metadata_values);
+		    if (!modified_metadata_xml_files.contains(metadata_xml_file)) {
+			modified_metadata_xml_files.add(metadata_xml_file);
+		    }
+		}
+	    }
+	}
+
+	// Let any listeners know that the metadata has changed
+	fireMetadataChangedEvent(file_nodes);
+    }
+
+
+    static public void removeMetadataChangedListener(MetadataChangedListener metadata_changed_listener)
+    {
+	metadata_changed_listeners.remove(metadata_changed_listener);
+    }
+
+
+    static public void replaceMetadata(CollectionTreeNode[] file_nodes, MetadataValue old_metadata_value, MetadataValue new_metadata_value)
+    {
+	// Replace the metadata in each file node in turn
+	for (int i = 0; i < file_nodes.length; i++) {
+	    File current_file = file_nodes[i].getFile();
+	    DebugStream.println("Replacing metadata in " + current_file.getAbsolutePath());
+
+	    // Find which metadata.xml file needs editing
+	    File current_file_directory = (current_file.isDirectory() ? current_file : current_file.getParentFile());
+	    String current_file_directory_path = current_file_directory.getAbsolutePath();
+	    for (int j = 0; j < metadata_xml_files.size(); j++) {
+		MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(j);
+
+		// This metadata.xml file is only applicable if it is at the same level as the file
+		if (current_file_directory_path.equals(metadata_xml_file.getParentFile().getAbsolutePath())) {
+		    metadata_xml_file.replaceMetadata(file_nodes[i], old_metadata_value, new_metadata_value);
+		    if (!modified_metadata_xml_files.contains(metadata_xml_file)) {
+			modified_metadata_xml_files.add(metadata_xml_file);
+		    }
+		}
+	    }
+	}
+
+	// Let any listeners know that the metadata has changed
+	fireMetadataChangedEvent(file_nodes);
+    }
+
+
+    /** Ensures that all the metadata is written to metadata.xml files. */
+    static public void saveMetadataXMLFiles()
+    {
+	// Save the file currently loaded into memory out to disk
+	MetadataXMLFile.saveLoadedFile();
+
+	// If the collection is stored on a remote server, upload all the modified files now
+	if (Gatherer.isGsdlRemote) {
+	    if (modified_metadata_xml_files.isEmpty()) {
+		DebugStream.println("No modified metadata.xml files to upload.");
+		return;
+	    }
+
+	    // Upload the files modified since last time, then reset the list
+	    Gatherer.remoteGreenstoneServer.uploadCollectionFiles(
+                  CollectionManager.getLoadedCollectionName(), (File[]) modified_metadata_xml_files.toArray(new File[0]));
+	    modified_metadata_xml_files.clear();
+	}
+    }
+
+
+    static public void unloadMetadataXMLFile(File metadata_xml_file_file)
+    {
+	DebugStream.println("Unloading metadata.xml file " + metadata_xml_file_file);
+
+	// Find the metadata.xml file in the list of loaded files, and remove it
+	for (int i = 0; i < metadata_xml_files.size(); i++) {
+	    MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(i);
+	    if (metadata_xml_file_file.getAbsolutePath().equals(metadata_xml_file.getAbsolutePath())) {
+		metadata_xml_files.remove(i);
+		break;
+	    }
+	}
+    }
+
+  
+	/**
+	  * Comparator to order MetadataXMLFiles in ascending order from
+	  * those in a higher level folder to those in a lower level folder
+	  * It is based on the assumption that all MetadataXMLFiles sent to
+	  * it to compare will be linear descendants of one toplevel folder
+	  * E.g. /A/metadata.xml, /A/B/metadata.xml, /A/B/C/D/metadata.xml.
+	  * In other words, that each is a substring of one of the others until we
+	  * the toplevel folder is reached.
+	*/
+	private static class MetadataXMLFileComparator implements Comparator {
+	    
+	    public int compare(Object o1, Object o2) {
+			if(!(o1 instanceof MetadataXMLFile)) {
+				return -1;
+			} else if (!(o2 instanceof MetadataXMLFile)) {
+				return 1;
+			} 
+
+			// Both are MetadataXMLFiles objects. Remove the terminating
+			// "metadata.xml" from their filenames to get their containing folder
+			String filename1 = ((MetadataXMLFile)o1).getParentFile().getAbsolutePath();
+			String filename2 = ((MetadataXMLFile)o2).getParentFile().getAbsolutePath();
+
+			// if 1 is a prefix of 2, then 1 < 2 in the ordering (1 comes before 2)
+			if(filename2.startsWith(filename1)) {
+				return -1;
+			} else if(filename1.startsWith(filename2)) {
+				return 1;
+			} else { 
+				// unlikely that the metadata.xml files will be the same
+				// or that neither is a prefix of the other
+				return filename1.compareTo(filename2); // sorts in ascending order
+			}      
+
+		}
+
+		public boolean equals(Object obj) {
+			if(!(obj instanceof MetadataXMLFileComparator)) {
+				return false;
+			} 
+
+			// else it is the same sort of comparator
+			return true;
+		}
+
+	}
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/ProfileXMLFile.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/ProfileXMLFile.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/ProfileXMLFile.java	(revision 31635)
@@ -0,0 +1,118 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+
+/** This class represents one profile.xml file */
+public class ProfileXMLFile
+    extends File
+{
+    private HashMap metadata_mapping = null;
+
+
+    public ProfileXMLFile(String profile_xml_file_path)
+    {
+	super(profile_xml_file_path);
+    }
+
+
+    public String getMetadataElementFor(String metadata_element_name_full)
+    {
+	if (getMetadataMapping() == null) {
+	    return null;
+	}
+
+	return (String) metadata_mapping.get(metadata_element_name_full);
+    }
+
+
+    public HashMap getMetadataMapping()
+    {
+	// If we have already loaded the metadata mapping, use it
+	if (metadata_mapping != null) {
+	    return metadata_mapping;
+	}
+
+	// Build the metadata mapping from the profile.xml file
+	metadata_mapping = new HashMap();
+
+	// Parse the profile.xml file
+	Document document = XMLTools.parseXMLFile(this);
+	if (document == null) {
+	    System.err.println("Error: Could not parse profile.xml file " + getAbsolutePath());
+	    return null;
+	}
+
+	// Read all the Action elements in the file
+	NodeList action_elements_nodelist = document.getElementsByTagName("Action");
+	for (int i = 0; i < action_elements_nodelist.getLength(); i++) {
+	    Element current_action_element = (Element) action_elements_nodelist.item(i);
+	    String source_metadata_element_name_full = current_action_element.getAttribute("source");
+	    String target_metadata_element_name_full = current_action_element.getAttribute("target");
+	    metadata_mapping.put(source_metadata_element_name_full, target_metadata_element_name_full);
+	}
+
+	return metadata_mapping;
+    }
+
+
+    public void mapElement(String source_metadata_element_name_full, String target_metadata_element_name_full)
+    {
+	// Parse the profile.xml file
+	Document document = XMLTools.parseXMLFile(this);
+	if (document == null) {
+	    System.err.println("Error: Could not parse profile.xml file " + getAbsolutePath());
+	    return;
+	}
+
+	// Create a new Action element to record this mapping
+	Element new_action_element = document.createElement("Action");
+	new_action_element.setAttribute("source", source_metadata_element_name_full);
+	new_action_element.setAttribute("target", target_metadata_element_name_full);
+	document.getDocumentElement().appendChild(new_action_element);
+
+	// Rewrite the profile.xml file
+	XMLTools.writeXMLFile(this, document);
+
+	// This is inefficient but for simplicity we'll just upload the file every time it is changed
+	if (Gatherer.isGsdlRemote) {
+	    Gatherer.remoteGreenstoneServer.uploadCollectionFile(CollectionManager.getLoadedCollectionName(), this);
+	}
+
+	// Invalidate the metadata mapping
+	metadata_mapping = null;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/ProfileXMLFileManager.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/ProfileXMLFileManager.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/metadata/ProfileXMLFileManager.java	(revision 31635)
@@ -0,0 +1,90 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.metadata;
+
+
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.util.XMLTools;
+
+
+/** This class is a static class that manages the profile.xml file */
+public class ProfileXMLFileManager
+{
+    static private ProfileXMLFile profile_xml_file = null;
+
+
+    static public void clearProfileXMLFile()
+    {
+	profile_xml_file = null;
+    }
+
+
+    static public void loadProfileXMLFile(File directory)
+    {
+	// Make sure the directory (metadata) exists
+	if (directory.exists() == false) {
+	    directory.mkdir();
+	}
+
+	File profile_xml_file_file = new File(directory, "profile.xml");
+	if (!profile_xml_file_file.exists()) {
+	    // Create a new (empty) profile.xml file in this directory...
+	    XMLTools.writeXMLFile(profile_xml_file_file, XMLTools.parseXMLFile("xml/profile.xml", true));
+	}
+
+	profile_xml_file = new ProfileXMLFile(profile_xml_file_file.getAbsolutePath());
+    }
+
+
+    static public String getMetadataElementFor(String metadata_element_name_full)
+    {
+	if (profile_xml_file == null) {
+	    return null;
+	}
+
+	return profile_xml_file.getMetadataElementFor(metadata_element_name_full);
+    }
+
+
+    static public HashMap getMetadataMapping()
+    {
+	if (profile_xml_file == null) {
+	    return null;
+	}
+
+	return profile_xml_file.getMetadataMapping();
+    }
+
+
+    static public void mapElement(String metadata_element_name_full, String target_metadata_element_name_full)
+    {
+	if (profile_xml_file != null) {
+	    profile_xml_file.mapElement(metadata_element_name_full, target_metadata_element_name_full);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ActionQueue.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ActionQueue.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ActionQueue.java	(revision 31635)
@@ -0,0 +1,194 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Greenstone Librarian Interface application, part of 
+ * the Greenstone digital library suite from the New Zealand Digital 
+ * Library Project at the University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.remote;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.zip.*;
+import javax.swing.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.FedoraInfo;
+import org.greenstone.gatherer.GAuthenticator;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.util.UnzipTools;
+import org.greenstone.gatherer.util.Utility;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.methods.multipart.FilePart;
+import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
+import org.apache.commons.httpclient.methods.multipart.Part;
+import org.apache.commons.httpclient.methods.multipart.*;
+import org.apache.commons.httpclient.params.*;
+import org.apache.commons.httpclient.HttpStatus;
+
+// ----------------------------------------------------------------------------------------------------
+//   Part of the RemoteGreenstoneServer's QUEUE LAYER
+// ----------------------------------------------------------------------------------------------------
+
+// Moved here. Previously called RemoteGreenstoneServerActionQueue
+/** 
+ * A Thread that maintains a queue of RemoteGreenstoneServer Actions
+ * that are to be executed in FIFO fashion.
+*/
+class ActionQueue extends Thread
+{
+    /** The queue of waiting jobs. */
+    private ArrayList queue = null;
+    private boolean exited;
+
+    public ActionQueue()
+    {
+	super("RemoteGreenstoneServerActionQueue");
+	exited = false;
+	if (Gatherer.isGsdlRemote) {
+	    queue = new ArrayList();
+	    start();
+	}
+    }
+    
+    
+    synchronized public void addAction(RemoteGreenstoneServerAction remote_greenstone_server_action)
+    {
+	queue.add(remote_greenstone_server_action);
+	notifyAll();
+    }
+    
+    
+    synchronized public int size()
+    {
+	return queue.size();
+    }
+    
+    synchronized public RemoteGreenstoneServerAction getAction(int i)
+    {
+	return (RemoteGreenstoneServerAction)queue.get(i);
+    }
+
+    synchronized public boolean hasExited() { 
+	return exited; 
+    }
+    
+    synchronized public void clear() { 
+	queue.clear(); 
+    }
+
+    public void run()
+    {
+	boolean exit = false;
+
+	while (!exit) {
+	    RemoteGreenstoneServerAction remote_greenstone_server_action = null;
+	    
+	    // Wait until we are notify()ed by addAction that there is a new job on the queue
+	    try {
+		if(Gatherer.remoteGreenstoneServer != null) {
+		    Gatherer.remoteGreenstoneServer.getProgressBar().setAction(null);
+		}
+		synchronized (this) {
+		    while(queue.size() <= 0) {
+			wait(); // wait for queue size to become > 0, which is done by external thread (performAction())
+		    }
+		    // Now there is at least one job on the queue, get the next in line and process it
+		    remote_greenstone_server_action = (RemoteGreenstoneServerAction) queue.get(0); //getAction(0) is already synchronized 
+		}
+	    } catch (InterruptedException exception) {
+		// It may be that GLI is exiting if we get here 
+		exit = true;
+	    }
+	    
+	    if(exit) {
+		break;
+	    }
+	    
+	    try {
+		remote_greenstone_server_action.perform();
+		
+		// No exceptions were thrown, so the action was successful
+		remote_greenstone_server_action.processed_successfully = true;
+	    }
+	    catch (RemoteGreenstoneServerAction.ActionCancelledException exception) {
+		remote_greenstone_server_action.processed_successfully = false;
+	    } 
+	    catch(java.net.ConnectException exception) {
+		if(exception.getMessage().trim().startsWith("Connection refused")) {
+		    exit = true;
+		} else {
+		    DebugStream.printStackTrace(exception);
+		}
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("RemoteGreenstoneServer.Error", exception.getMessage()), Dictionary.get("RemoteGreenstoneServer.Error_Title"), JOptionPane.ERROR_MESSAGE);
+		remote_greenstone_server_action.processed_successfully = false;			
+	    }
+	    catch (FileNotFoundException exception) {
+		// FileNotFoundException happens when there's no GS server at the user-provided
+		// url (the address of gliserver.pl is wrong).
+		exit = true;
+		DebugStream.printStackTrace(exception);
+		JOptionPane.showMessageDialog(Gatherer.g_man, 
+					      Dictionary.get("RemoteGreenstoneServer.Error", 
+							     "No gliserver.pl found. " + exception.getMessage()), 
+					      Dictionary.get("RemoteGreenstoneServer.Error_Title"), 
+					      JOptionPane.ERROR_MESSAGE);
+		remote_greenstone_server_action.processed_successfully = false;
+	    }
+	    catch (Exception exception) {
+		DebugStream.printStackTrace(exception);
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("RemoteGreenstoneServer.Error", exception.getMessage()), Dictionary.get("RemoteGreenstoneServer.Error_Title"), JOptionPane.ERROR_MESSAGE);
+		remote_greenstone_server_action.processed_successfully = false;
+	    }
+	    
+	    // We're done with this action, for better or worse
+	    try {
+		remote_greenstone_server_action.processed = true;
+		synchronized(remote_greenstone_server_action) {
+		    remote_greenstone_server_action.notifyAll(); // notifies RemoteGreenstoneServer.performAction()
+		}
+	    } catch (Exception exception) {
+		System.err.println("RemoteGreenstoneServerActionQueue.run() - exception: " + exception);
+	    }
+	    synchronized (this) { // remove the action just processed from the queue, since it's done.
+		queue.remove(0);
+	    }
+	}
+
+	// Out of while, means exit = true
+	// Stop the gazillion annoying error messages when the connection was simply
+	// refused or when the user pressed Cancel in the opening dialog, by clearing
+	// the action queue of subsequent actions and setting exited to true.
+	synchronized(this) {
+	    queue.clear();
+	    exited = true;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/RemoteGreenstoneServer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/RemoteGreenstoneServer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/RemoteGreenstoneServer.java	(revision 31635)
@@ -0,0 +1,833 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.remote;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.zip.*;
+import javax.swing.*;
+import java.io.ByteArrayOutputStream;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.FedoraInfo;
+import org.greenstone.gatherer.GAuthenticator;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.util.UnzipTools;
+import org.greenstone.gatherer.util.Utility;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.methods.multipart.FilePart;
+import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
+import org.apache.commons.httpclient.methods.multipart.Part;
+import org.apache.commons.httpclient.methods.multipart.*;
+import org.apache.commons.httpclient.params.*;
+import org.apache.commons.httpclient.HttpStatus;
+
+
+public class RemoteGreenstoneServer
+{
+    // A PasswordAuthentication object is created whenever it is required
+    static private PasswordAuthentication remote_greenstone_server_authentication = null;
+    // static private PasswordAuthentication remote_greenstone_server_authentication = new PasswordAuthentication(System.getProperty("user.name"), new char[] { });
+
+    // the language and region environment variables (in "lang_REGION" form)
+    // this is necessary in order for the client and server sides to zip and unzip
+    // using the same settings
+    public final String lang_region;
+
+    private ActionQueue remote_greenstone_server_action_queue;
+    private RemoteGreenstoneServer.ProgressBar progress_bar;
+
+    public RemoteGreenstoneServer() {
+	// Create the progress_bar first since ActionQueue uses it in its thread 
+	// (the thread will start immediately).
+	progress_bar = new RemoteGreenstoneServer.ProgressBar();
+	remote_greenstone_server_action_queue = new ActionQueue();
+
+	String langReg = System.getenv("LANG");
+	lang_region = (langReg == null) ? "" : langReg;	
+    }
+
+    // ----------------------------------------------------------------------------------------------------
+    //   PUBLIC LAYER
+    // ----------------------------------------------------------------------------------------------------
+    public String deleteCollection(String collection_name)
+    {
+	return performAction(new RemoteGreenstoneServerAction.DeleteCollectionAction(collection_name));
+    }
+
+
+    public String deleteCollectionFile(String collection_name, File collection_file)
+    {
+	return performAction(new RemoteGreenstoneServerAction.DeleteCollectionFileAction(collection_name, collection_file));
+    }
+
+
+    public String downloadCollection(String collection_name)
+    {
+	return performAction(new RemoteGreenstoneServerAction.DownloadCollectionAction(collection_name));
+    }
+
+
+    public String downloadCollectionArchives(String collection_name)
+    {
+	return performAction(new RemoteGreenstoneServerAction.DownloadCollectionArchivesAction(collection_name));
+    }
+
+
+    public String downloadCollectionConfigurations()
+    {
+	return performAction(new RemoteGreenstoneServerAction.DownloadCollectionConfigurationsAction());
+    }
+
+
+    public String downloadCollectionFile(String collection_name, File collection_file)
+    {
+	return performAction(new RemoteGreenstoneServerAction.DownloadCollectionFileAction(collection_name, collection_file));
+    }
+    
+    // get web.xml from the server -- for a remote gli of GS3
+    public String downloadWebXMLFile()
+    {
+	return performAction(new RemoteGreenstoneServerAction.DownloadWebXMLFileAction());
+    }
+
+    public String getScriptOptions(String script_name, String script_arguments)
+    {
+	return performAction(new RemoteGreenstoneServerAction.GetScriptOptionsAction(script_name, script_arguments));
+    }
+    
+    //get all available site names from the server -- for a remote gli of GS3
+    public String getSiteNames()
+    {
+	return performAction(new RemoteGreenstoneServerAction.GetSiteNamesAction());
+    }
+
+    public String moveCollectionFile(String collection_name, File source_collection_file, File target_collection_file)
+    {
+	return performAction(new RemoteGreenstoneServerAction.MoveCollectionFileAction(
+                             collection_name, source_collection_file, target_collection_file));
+    }
+
+
+    public String newCollectionDirectory(String collection_name, File new_collection_directory)
+    {
+	return performAction(new RemoteGreenstoneServerAction.NewCollectionDirectoryAction(
+                             collection_name, new_collection_directory));
+    }
+
+
+    public String runScript(String collection_name, String script_name, String script_arguments, GShell shell)
+    {
+	return performAction(new RemoteGreenstoneServerAction.RunScriptAction(
+                             collection_name, script_name, script_arguments, shell));
+    }
+
+
+    public String uploadCollectionFile(String collection_name, File collection_file)
+    {
+	return performAction(new RemoteGreenstoneServerAction.UploadCollectionFilesAction(
+                             collection_name, new File[] { collection_file }));
+    }
+
+
+    public String uploadCollectionFiles(String collection_name, File[] collection_files)
+    {
+	return performAction(new RemoteGreenstoneServerAction.UploadCollectionFilesAction(collection_name, collection_files));
+    }
+
+
+    public String uploadFilesIntoCollection(String collection_name, File[] source_files, File target_collection_directory)
+    {
+	return performAction(new RemoteGreenstoneServerAction.UploadFilesIntoCollectionAction(
+                             collection_name, source_files, target_collection_directory));
+    }
+
+    public boolean exists(String collection_name, File collection_file)
+    {
+	String result = performAction(new RemoteGreenstoneServerAction.ExistsAction(collection_name, collection_file));
+	if(result.indexOf("exists") != -1) {
+	    return true;
+	}
+	else if(result.indexOf("does not exist") != -1) {
+	    return false;
+	}
+	return false;
+    }
+
+    public int getGreenstoneVersion()
+    {
+	// returns message "Greenstone version is: <version number of the Greenstone remote server>"
+	String result = performAction(new RemoteGreenstoneServerAction.VersionAction());
+	int index = result.indexOf(":"); 
+	if(index != -1) {
+	    result = result.substring(index+1).trim(); // skip the space after colon, must remove surrounding spaces
+	}
+	// if space is returned, then the request failed (the server may not have been running)
+	int greenstoneVersion = result.equals("") ? -1 : Integer.parseInt(result);
+	return greenstoneVersion;
+    }
+
+    /** For constructing the preview command (the library URL) with */
+    public String getLibraryURL(String serverHomeURL)
+    {
+	// returns message "Greenstone library URL suffix is: <e.g. /greenstone3/library or /gsdl/cgi-bin/library>"
+	String libSuffix = performAction(new RemoteGreenstoneServerAction.LibraryURLSuffixAction());
+	int index = libSuffix.indexOf(":"); 
+	if(index != -1) {
+	    libSuffix = libSuffix.substring(index+1).trim(); // skip the space after colon and remove surrounding spaces
+	}
+	
+	// serverHomeURL is of the form, http://domain/other/stuff. We want the prefix upto & including domain
+	// and prepend that to the libraryURLSuffix
+	index = -1;
+	for(int i = 0; i < 3; i++) {
+	    index = serverHomeURL.indexOf("/", index+1);
+	    if(index == -1) { // shouldn't happen, but if it does, we'll be in an infinite loop
+		break; 
+	    }
+	}
+	serverHomeURL = serverHomeURL.substring(0, index);
+	return serverHomeURL + libSuffix;
+    }
+
+    // ----------------------------------------------------------------------------------------------------
+
+
+    public void exit()
+    {
+	System.err.println("Exiting, number of jobs on queue: " + remote_greenstone_server_action_queue.size());
+
+	// If there are still jobs on the queue we must wait for the jobs to finish
+	while (remote_greenstone_server_action_queue.size() > 0) {
+	    synchronized (remote_greenstone_server_action_queue) {
+		try {
+		    DebugStream.println("Waiting for queue to become empty...");
+		    remote_greenstone_server_action_queue.wait(500);
+		}
+		catch (InterruptedException exception) {}
+	    }
+	}
+    }
+    
+
+    // ----------------------------------------------------------------------------------------------------
+    //   QUEUE LAYER
+    // ----------------------------------------------------------------------------------------------------
+
+
+    /** Returns null if we cannot wait for the action to finish, "" if the action failed, or the action output. */
+    private String performAction(RemoteGreenstoneServerAction remote_greenstone_server_action)
+    {
+	// Check for whether the queue thread stopped running because 
+	// the user cancelled out. If so, exit GLI.
+	if(remote_greenstone_server_action_queue.hasExited()) {
+	    remote_greenstone_server_action_queue.clear();
+	    //remote_greenstone_server_action_queue = null;
+	    Gatherer.exit();
+	    return "";
+	}
+
+	// Add the action to the queue
+	remote_greenstone_server_action_queue.addAction(remote_greenstone_server_action);
+	String threadName = Thread.currentThread().getName();
+
+	// If we're running in the GUI thread we must return immediately
+	// We cannot wait for the action to complete because this will block any GUI updates
+	if (SwingUtilities.isEventDispatchThread()) {
+	    System.err.println(threadName 
+			       + "\tACTION QUEUED: In event dispatch thread, returning immediately...\n\t" 
+			       + remote_greenstone_server_action);
+	    return null;
+	}
+
+	// Otherwise wait until the action is processed
+	try {
+	    synchronized (remote_greenstone_server_action) {
+		while(!remote_greenstone_server_action.processed) {
+		    //System.err.println("Waiting for action to complete...: " + remote_greenstone_server_action);
+		    DebugStream.println("Waiting for action to complete...");
+		    remote_greenstone_server_action.wait(); // wait to be notified when the action has been processed 
+		}
+	    }
+	} catch (Exception e) {
+	    System.err.println("RemoteGreenstoneServer.performAction() - exception: " + e);
+	    e.printStackTrace();
+	}
+
+	// Return "" if the action failed
+	if (!remote_greenstone_server_action.processed_successfully) {
+	    return "";
+	}
+	// Otherwise return the action output
+	return remote_greenstone_server_action.action_output;
+    }
+
+
+    // ----------------------------------------------------------------------------------------------------
+    //   PROGRESS BAR
+    // ----------------------------------------------------------------------------------------------------
+
+    static class ProgressBar
+	extends JProgressBar
+    {
+	public ProgressBar()
+	{
+	    setBackground(Configuration.getColor("coloring.collection_tree_background", false));
+	    setForeground(Configuration.getColor("coloring.collection_tree_foreground", false));
+	    setString(Dictionary.get("FileActions.No_Activity"));
+	    setStringPainted(true);
+	}
+
+	/** synchronized to avoid conflicts since several threads access this */
+	synchronized public void setAction(String action)
+	{
+	    if (action != null) {
+		DebugStream.println(action);
+	    }
+
+	    // We cannot call this from the GUI thread otherwise the progress bar won't start
+	    if (SwingUtilities.isEventDispatchThread()) {
+		System.err.println("ERROR: RemoteGreenstoneServerProgressBar.setAction() called from event dispatch thread!");
+		return;
+	    }
+
+	    // Set the string on the progress bar, and start or stop it
+	    if (action == null) {
+		setString(Dictionary.get("FileActions.No_Activity"));
+		setIndeterminate(false);
+	    }
+	    else {
+		setString(action);
+		setIndeterminate(true);
+	    }
+	}
+    }
+    
+    /** synchronized to avoid conflicts since several threads access this */
+    synchronized public RemoteGreenstoneServer.ProgressBar getProgressBar()
+    {
+	return progress_bar;
+    }
+
+
+    // ----------------------------------------------------------------------------------------------------
+    //   AUTHENTICATION LAYER
+    // ----------------------------------------------------------------------------------------------------
+
+    static private class RemoteGreenstoneServerAuthenticateTask
+	extends Thread
+    {
+	public void run()
+	{
+	    remote_greenstone_server_authentication = new RemoteGreenstoneServerAuthenticator().getAuthentication();
+	}
+    
+
+	static private class RemoteGreenstoneServerAuthenticator
+	    extends GAuthenticator
+	{
+	    public PasswordAuthentication getAuthentication(String username, String password)
+	    {
+		return getPasswordAuthentication(username,password);
+	    }
+
+	    public PasswordAuthentication getAuthentication()
+	    {
+		return getPasswordAuthentication();
+	    }
+
+	    protected String getMessageString()
+	    {
+		if (Gatherer.GS3){
+		    return (Dictionary.get("RemoteGreenstoneServer.Authentication_Message_gs3") + " " + Configuration.site_name);
+		}
+		return Dictionary.get("RemoteGreenstoneServer.Authentication_Message");
+	    }
+ 	}
+    }
+    
+    public void set_remote_greenstone_server_authentication_to_null() {
+	remote_greenstone_server_authentication = null;
+    }
+
+    private void authenticateUser()
+	throws RemoteGreenstoneServerAction.ActionCancelledException
+    {
+	// If we don't have any authentication information then ask for it now
+	if (remote_greenstone_server_authentication == null) {
+	    try {
+		// We have to do this on the GUI thread
+		SwingUtilities.invokeAndWait(new RemoteGreenstoneServerAuthenticateTask());
+	    }
+	    catch (Exception exception) {
+		System.err.println("Exception occurred when authenticating the user: " + exception);
+		DebugStream.printStackTrace(exception);
+	    }
+
+	    // If it is still null then the user has cancelled the authentication, so the action is cancelled
+	    if (remote_greenstone_server_authentication == null) {
+		throw new RemoteGreenstoneServerAction.ActionCancelledException();
+	    }
+	}
+    }
+
+
+    public String getUsername()
+    {
+	if (remote_greenstone_server_authentication != null) {
+	    return remote_greenstone_server_authentication.getUserName();
+	}
+
+	return null;
+    }
+
+
+    // ----------------------------------------------------------------------------------------------------
+    //   REQUEST LAYER
+    // ----------------------------------------------------------------------------------------------------
+
+
+    /** Returns the command output if the action completed, throws some kind of exception otherwise. */
+    String downloadFile(String gliserver_args, String file_path)
+	throws Exception
+    {
+	while (true) {
+	    // Check that Configuration.gliserver_url is set
+	    if (Configuration.gliserver_url == null) {
+		throw new Exception("Empty gliserver URL: please set this in Preferences before continuing.");
+	    }
+
+	    // Ask for authentication information (if necessary), then perform the action
+	    authenticateUser();
+	    String gliserver_url_string = Configuration.gliserver_url.toString();
+	    String command_output = downloadFileInternal(gliserver_url_string, gliserver_args, file_path);
+
+	    // Debugging - print any ok messages to stderr
+	    //System.err.println("**** RECEIVED (sendCommandToServer()): " + command_output);
+
+	    // Check the first line to see if authentication has failed; if so, go around the loop again
+	    if (command_output.startsWith("ERROR: Authentication failed:")) {
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("RemoteGreenstoneServer.Error", command_output.substring("ERROR: ".length())), Dictionary.get("RemoteGreenstoneServer.Error_Title"), JOptionPane.ERROR_MESSAGE);
+		remote_greenstone_server_authentication = null;
+		continue;
+	    }
+	    // Check if the collection is locked by someone else; if so, see if the user wants to steal the lock
+	    else if (command_output.startsWith("ERROR: Collection is locked by: ")) {
+		if (JOptionPane.showConfirmDialog(Gatherer.g_man, Dictionary.get("RemoteGreenstoneServer.Steal_Lock_Message", command_output.substring("ERROR: Collection is locked by: ".length())), Dictionary.get("RemoteGreenstoneServer.Error_Title"), JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION) {
+		    // The user has decided to cancel the action
+		    throw new RemoteGreenstoneServerAction.ActionCancelledException();
+		}
+
+		// The user has decided to steal the lock... rerun the command with "&steal_lock="
+		gliserver_args += "&steal_lock=";
+		continue;
+	    }
+	    // Handle other types of errors by throwing an exception
+	    else if (command_output.startsWith("ERROR: ")) {
+		throw new Exception(command_output.substring("ERROR: ".length()));
+	    }
+
+	    // There were no exceptions thrown so the action must have succeeded
+	    return command_output;
+	}
+    }
+
+    /** Returns true or false depending on whether authentication is required for the cmd 
+     * string embedded in the given gliserver_args. No authentication is required for either 
+     * of the commands greenstone-server-version and get-library-url-suffix. */
+    private boolean isAuthenticationRequired(String gliserver_args) {
+	return ((gliserver_args.indexOf("greenstone-server-version") == -1) 
+		&& (gliserver_args.indexOf("get-library-url-suffix") == -1));
+    }
+
+
+    /** Returns the command output if the action completed, throws some kind of exception otherwise. 
+     *  Package access, so that RemoteGreenstoneServerAction.java can call this.
+     */
+    String sendCommandToServer(String gliserver_args, GShell shell)
+	throws Exception
+    {
+	while (true) {
+	    
+	    // Check that Configuration.gliserver_url is set
+	    if (Configuration.gliserver_url == null) {
+		throw new Exception("Empty gliserver URL: please set this in Preferences before continuing.");
+	    }
+
+	    // Ask for authentication information (if necessary), then perform the action
+	    if(isAuthenticationRequired(gliserver_args)) {
+		try {
+		    authenticateUser();
+		} catch (RemoteGreenstoneServerAction.ActionCancelledException e) {
+		    // Authentication popup only appears at the start. If the user cancelled
+		    // out of it, then another remote action always remains on the queue,
+		    // preventing a clean exit. Need to clear queue before exit.
+		    synchronized (remote_greenstone_server_action_queue) {			
+			remote_greenstone_server_action_queue.clear();			
+		    }
+		    Gatherer.exit();
+		}
+	    }
+	    String gliserver_url_string = Configuration.gliserver_url.toString();
+	    String command_output = sendCommandToServerInternal(gliserver_url_string, gliserver_args, shell);
+	    	    
+	    // Debugging - print any ok messages to stderr
+	    //if(!(command_output.trim().startsWith("<"))) {
+	    //System.err.println("**** RECEIVED (sendCommandToServer()): " + command_output);
+	    //}
+		
+	    // Check the first line to see if authentication has failed; if so, go around the loop again
+	    if (command_output.startsWith("ERROR: Authentication failed:")) {
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("RemoteGreenstoneServer.Error", command_output.substring("ERROR: ".length())), Dictionary.get("RemoteGreenstoneServer.Error_Title"), JOptionPane.ERROR_MESSAGE);
+		remote_greenstone_server_authentication = null;
+		continue;
+	    }
+	    // Check if the collection is locked by someone else; if so, see if the user wants to steal the lock
+	    else if (command_output.startsWith("ERROR: Collection is locked by: ")) {
+		if (JOptionPane.showConfirmDialog(Gatherer.g_man, Dictionary.get("RemoteGreenstoneServer.Steal_Lock_Message", command_output.substring("ERROR: Collection is locked by: ".length())), Dictionary.get("RemoteGreenstoneServer.Error_Title"), JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION) {
+		    // The user has decided to cancel the action
+		    throw new RemoteGreenstoneServerAction.ActionCancelledException();
+		}
+
+		// The user has decided to steal the lock... rerun the command with "&steal_lock="
+		gliserver_args += "&steal_lock=";
+		continue;
+	    }
+	    // Handle other types of errors by throwing an exception
+	    else if (command_output.startsWith("ERROR: ")) { 
+		throw new Exception(command_output.substring("ERROR: ".length()));
+	    } else if (command_output.indexOf("ERROR: ") != -1) { // check if ERROR occurs anywhere else in the output
+		throw new Exception(command_output);
+	    }
+
+	    
+	    // There were no exceptions thrown so the action must have succeeded
+	    return command_output;
+	}
+    }
+
+
+    /** Returns the command output if the action completed, throws some kind of exception otherwise. */
+    String uploadFile(String gliserver_args, String file_path)
+	throws Exception
+    {
+	while (true) {
+	    // Check that Configuration.gliserver_url is set
+	    if (Configuration.gliserver_url == null) {
+		throw new Exception("Empty gliserver URL: please set this in Preferences before continuing.");
+	    }
+
+	    // Ask for authentication information (if necessary), then perform the action
+	    authenticateUser();
+	    String gliserver_url_string = Configuration.gliserver_url.toString();
+	    String command_output = uploadFileInternal(gliserver_url_string, gliserver_args, file_path);
+	    // System.err.println("Command output: " + command_output);
+
+	    // Check the first line to see if authentication has failed; if so, go around the loop again
+	    if (command_output.startsWith("ERROR: Authentication failed:")) {
+		JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("RemoteGreenstoneServer.Error", command_output.substring("ERROR: ".length())), Dictionary.get("RemoteGreenstoneServer.Error_Title"), JOptionPane.ERROR_MESSAGE);
+		remote_greenstone_server_authentication = null;
+		continue;
+	    }
+	    // Check if the collection is locked by someone else; if so, see if the user wants to steal the lock
+	    else if (command_output.startsWith("ERROR: Collection is locked by: ")) {
+		if (JOptionPane.showConfirmDialog(Gatherer.g_man, Dictionary.get("RemoteGreenstoneServer.Steal_Lock_Message", command_output.substring("ERROR: Collection is locked by: ".length())), Dictionary.get("RemoteGreenstoneServer.Error_Title"), JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION) {
+		    // The user has decided to cancel the action
+		    throw new RemoteGreenstoneServerAction.ActionCancelledException();
+		}
+
+		// The user has decided to steal the lock... rerun the command with "&steal_lock="
+		gliserver_args += "&steal_lock=";
+		continue;
+	    }
+	    // Handle other types of errors by throwing an exception
+	    else if (command_output.startsWith("ERROR: ")) {
+		throw new Exception(command_output.substring("ERROR: ".length()));
+	    }
+
+	    // There were no exceptions thrown so the action must have succeeded
+	    return command_output;
+	}
+    }
+
+
+    // ----------------------------------------------------------------------------------------------------
+    //   NETWORK LAYER
+    // ----------------------------------------------------------------------------------------------------
+
+
+    /** Returns the command output if the action completed, throws some kind of exception otherwise. */
+    private String downloadFileInternal(String download_cgi, String cgi_args, String file_path)
+	throws Exception
+    {
+	DebugStream.println("gliserver URL: " + download_cgi);
+	System.err.println("gliserver args: " + cgi_args);
+
+	// Add username and password, and a timestamp
+	cgi_args += "&un=" + remote_greenstone_server_authentication.getUserName();
+	cgi_args += "&pw=" + new String(remote_greenstone_server_authentication.getPassword());
+	cgi_args += "&ts=" + System.currentTimeMillis();
+	if (Gatherer.GS3){
+	    cgi_args += "&site=" + Configuration.site_name;
+	}
+
+	URL download_url = new URL(download_cgi);
+	URLConnection dl_connection = download_url.openConnection();
+	dl_connection.setDoOutput(true);
+	OutputStream dl_os = dl_connection.getOutputStream();
+
+	PrintWriter dl_out = new PrintWriter(dl_os);
+	dl_out.println(cgi_args);
+	dl_out.close();
+
+	// Download result from running cgi script
+	InputStream dl_is = dl_connection.getInputStream();
+	BufferedInputStream dl_bis = new BufferedInputStream(dl_is);
+	DataInputStream dl_dbis = new DataInputStream(dl_bis);
+
+	String first_line = "";
+	byte[] buf = new byte[1024];
+	int len = dl_dbis.read(buf);
+	if (len >= 0) {
+	    String first_chunk = new String(buf, 0, len);
+	    // first_line = first_chunk.substring(0, ((first_chunk.indexOf("\n") != -1) ? first_chunk.indexOf("\n") : len));
+	    first_line = first_chunk.substring(0, ((first_chunk.indexOf("\n") != -1) ? first_chunk.indexOf("\n") : ((first_chunk.length()<len) ? first_chunk.length():len)));
+	    // Save the data to file
+	    FileOutputStream zip_fos = new FileOutputStream(file_path);
+	    BufferedOutputStream zip_bfos = new BufferedOutputStream(zip_fos);
+
+	    while (len >= 0) {
+		zip_bfos.write(buf, 0, len);
+		len = dl_dbis.read(buf);
+	    }
+
+	    zip_bfos.close();
+	    zip_fos.close();
+	}
+
+	dl_dbis.close();
+	dl_bis.close();
+	dl_is.close();
+	return first_line;
+    }
+
+
+    /** Returns the command output if the action completed, throws some kind of exception otherwise. */
+    private String sendCommandToServerInternal(String gliserver_url_string, String cgi_args, GShell shell)
+	throws Exception
+    {
+	DebugStream.println("gliserver URL: " + gliserver_url_string);
+	System.err.println("gliserver args: " + cgi_args);
+
+	// Add username and password, and a timestamp
+	if(isAuthenticationRequired(cgi_args)) {
+	    cgi_args += "&un=" + remote_greenstone_server_authentication.getUserName();
+	    cgi_args += "&pw=" + new String(remote_greenstone_server_authentication.getPassword());
+	}
+	cgi_args += "&ts=" + System.currentTimeMillis();
+	if (Gatherer.GS3){
+	    cgi_args += "&site=" + Configuration.site_name;
+	}
+
+	URL gliserver_url = new URL(gliserver_url_string + "?" + cgi_args);
+	URLConnection gliserver_connection = gliserver_url.openConnection();
+
+	// Read the output of the command from the server, and return it
+	StringBuffer command_output_buffer = new StringBuffer(2048);
+	InputStream gliserver_is = gliserver_connection.getInputStream();
+	BufferedReader gliserver_in = new BufferedReader(new InputStreamReader(gliserver_is, "UTF-8"));
+	String gliserver_output_line = gliserver_in.readLine();
+	while (gliserver_output_line != null) {
+	    if (shell != null) {
+		shell.fireMessage(gliserver_output_line);
+		if (shell.hasSignalledStop()) {
+		    throw new RemoteGreenstoneServerAction.ActionCancelledException();
+		}
+	    }
+	    command_output_buffer.append(gliserver_output_line + "\n");
+	    gliserver_output_line = gliserver_in.readLine();
+	}
+	gliserver_in.close();
+
+	return command_output_buffer.toString();
+    }
+
+	       
+    /** Returns the command output if the action completed, throws some kind of exception otherwise. */
+    private String uploadFileInternal(String upload_cgi, String cgi_args, String file_path)
+	throws Exception
+    {
+	System.err.println("gliserver URL: " + upload_cgi);
+	System.err.println("gliserver args: " + cgi_args);
+
+	//For a remote GS3
+	//GS3 is running on Tomcat, and Tomcat requires a connection timeout to be set up at the client
+	//side while uploading files. As HttpURLConnection couldn't set the connection timeout, HttpClient.jar 
+	//from Jakarta is applied to solve this problem only for uploading files. 
+	if (Gatherer.GS3) {
+
+	    // Setup the POST method
+	    PostMethod httppost = new PostMethod(upload_cgi+"?"+cgi_args); // cgi_args: QUERY_STRING on perl server side
+	    		    
+	    // construct the multipartrequest form
+	    String[] cgi_array=cgi_args.split("&");// get parameter-value pairs from cgi_args string
+	    Part[] parts=new Part[cgi_array.length+5];
+	    
+	    // The FilePart: consisting of the (cgi-arg) name and (optional) filename in the Content-Disposition 
+	    // of the POST request Header (see CGI.pm), and the actual zip file itself. It uses the defaults:
+	    // Content-Type: application/octet-stream; charset=ISO-8859-1,Content-Transfer-Encoding: binary
+	    parts[0]= new FilePart("uploaded_file", "zipFile", new File(file_path));
+
+	    parts[1]= new StringPart("un", remote_greenstone_server_authentication.getUserName());
+	    parts[2]= new StringPart("pw", new String(remote_greenstone_server_authentication.getPassword()));
+	    parts[3]= new StringPart("ts", String.valueOf(System.currentTimeMillis()));
+	    parts[4]= new StringPart("site", Configuration.site_name);
+	    // find all parameters of cgi-args and add them into Part[]
+	    for (int i=0; i<cgi_array.length;i++){
+		parts[5+i]=new StringPart(cgi_array[i].substring(0,cgi_array[i].indexOf("=")),cgi_array[i].substring(cgi_array[i].indexOf("=")+1,cgi_array[i].length()));
+	    }
+
+	    // set MultipartRequestEntity on the POST method
+	    httppost.setRequestEntity(new MultipartRequestEntity(parts, httppost.getParams()));
+
+	    // See file gli/request.txt for the multipart request that's been generated:
+	    //httppost.getRequestEntity().writeRequest(new FileOutputStream("request.txt", true)); // true: appends
+	    
+	    //set up the HttpClient connection
+	    HttpClient client=new HttpClient();	
+	    client.getParams().setBooleanParameter(HttpMethodParams.USE_EXPECT_CONTINUE, false);
+	    client.getParams().setConnectionManagerTimeout(200000); //set the connection timeout
+	    // get the output of the command from the server
+	    String command_output = ""; 
+	    try{
+		client.executeMethod(httppost);
+		if (httppost.getStatusCode() == HttpStatus.SC_OK) {
+		    command_output = httppost.getStatusLine().toString();
+		} else {
+		    command_output = httppost.getStatusLine().toString();
+		    System.out.println("Unexpected failure: " + httppost.getStatusLine().toString());
+		}
+	    }catch(IOException e){
+		e.printStackTrace();
+	    }finally{
+		httppost.releaseConnection();
+	    }
+	    return command_output;
+	}
+
+	//For a remote GS2
+	// Add username and password, and a timestamp
+	cgi_args += "&un=" + remote_greenstone_server_authentication.getUserName();
+	cgi_args += "&pw=" + new String(remote_greenstone_server_authentication.getPassword());
+	cgi_args += "&ts=" + System.currentTimeMillis();
+
+	// Open a HTTP connection to the URL
+	URL url = new URL(upload_cgi);
+	HttpURLConnection gliserver_connection = (HttpURLConnection) url.openConnection();
+
+	gliserver_connection.setDoInput(true);         // Allow Inputs
+	gliserver_connection.setDoOutput(true);        // Allow Outputs
+	gliserver_connection.setUseCaches(false);      // Don't use a cached copy.
+
+	gliserver_connection.setRequestProperty("Connection", "Keep-Alive");	    
+
+	// Send zip file to server
+	File file = new File(file_path);
+	FileInputStream fileInputStream = new FileInputStream(file);
+
+	// Add file size argument, because IIS 6 needs a lot of help
+	int file_size = fileInputStream.available();
+	cgi_args += "&fs=" + file_size;
+
+	DataOutputStream dos = new DataOutputStream(gliserver_connection.getOutputStream());
+	dos.writeBytes(cgi_args + "\n");
+
+	// create a buffer of maximum size
+	final int maxBufferSize = 1024;
+	int bytesAvailable = file_size;
+	int bufferSize = Math.min(bytesAvailable, maxBufferSize);
+	byte[] buffer = new byte[bufferSize];
+
+	// read file and write it into form...
+	// !! This uses a lot of memory when the file being uploaded is big -- Java seems to need to keep
+	//   the entire file in the DataOutputStream? (Use Runtime.getRuntime().totalMemory() to see)
+	int bytesRead = fileInputStream.read(buffer, 0, bufferSize);
+	while (bytesRead > 0) {
+	    dos.write(buffer, 0, bytesRead);
+	    bytesAvailable = fileInputStream.available();
+	    bufferSize = Math.min(bytesAvailable, maxBufferSize);
+	    bytesRead = fileInputStream.read(buffer, 0, bufferSize);
+	}
+
+	// close streams
+	fileInputStream.close();
+	dos.flush();
+	dos.close();
+
+	// Read the output of the command from the server, and return it
+	String command_output = "";
+	InputStream gliserver_is = gliserver_connection.getInputStream();
+	BufferedReader gliserver_in = new BufferedReader(new InputStreamReader(gliserver_is, "UTF-8"));
+	String gliserver_output_line = gliserver_in.readLine();
+	while (gliserver_output_line != null) {
+	    command_output += gliserver_output_line + "\n";
+	    gliserver_output_line = gliserver_in.readLine();
+	}
+	gliserver_in.close();
+
+	return command_output;
+    }
+
+
+    // ----------------------------------------------------------------------------------------------------
+    //   UTILITIES
+    // ----------------------------------------------------------------------------------------------------
+
+
+    public String getPathRelativeToDirectory(File file, String directory_path)
+    {
+	String file_path = file.getAbsolutePath();
+	if (!file_path.startsWith(directory_path)) {
+	    System.err.println("ERROR: File path " + file_path + " is not a child of " + directory_path);
+	    return file_path;
+	}
+
+	String relative_file_path = file_path.substring(directory_path.length());
+	if (relative_file_path.startsWith(File.separator)) {
+	    relative_file_path = relative_file_path.substring(File.separator.length());
+	}
+	return relative_file_path;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/RemoteGreenstoneServerAction.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/RemoteGreenstoneServerAction.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/RemoteGreenstoneServerAction.java	(revision 31635)
@@ -0,0 +1,664 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Greenstone Librarian Interface application, part of 
+ * the Greenstone digital library suite from the New Zealand Digital 
+ * Library Project at the University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+*/
+
+package org.greenstone.gatherer.remote;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.zip.*;
+import javax.swing.*;
+import java.io.ByteArrayOutputStream;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.FedoraInfo;
+import org.greenstone.gatherer.GAuthenticator;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.feedback.Base64;
+import org.greenstone.gatherer.shell.GShell;
+import org.greenstone.gatherer.util.UnzipTools;
+import org.greenstone.gatherer.util.Utility;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.methods.multipart.FilePart;
+import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
+import org.apache.commons.httpclient.methods.multipart.Part;
+import org.apache.commons.httpclient.methods.multipart.*;
+import org.apache.commons.httpclient.params.*;
+import org.apache.commons.httpclient.HttpStatus;
+
+
+// Code moved here from RemoteGreenstoneServer.java
+// ----------------------------------------------------------------------------------------------------
+//   ACTIONS
+// ----------------------------------------------------------------------------------------------------
+/** RemoteGreenstoneServer Actions that can go into the remote GS server's  ActionQueue.
+ * Contains many package access inner classes that are Actions.   
+*/
+public abstract class RemoteGreenstoneServerAction
+{
+    public String action_output = null;
+    public boolean processed = false;
+    public boolean processed_successfully;
+    
+    protected RemoteGreenstoneServer remote = null;
+    protected RemoteGreenstoneServer.ProgressBar progress_bar = null;
+
+    public RemoteGreenstoneServerAction() {
+	remote = Gatherer.remoteGreenstoneServer;
+	progress_bar = remote.getProgressBar();
+    }
+
+    abstract public void perform()
+	throws Exception;
+
+    /*
+      protected String sendCommandToServer(String gliserver_args, GShell shell) 
+      throws Exception
+      {
+      return Gatherer.remoteGreenstoneServer.sendCommandToServer(gliserver_args, shell);
+      }
+      
+      protected void setAction(String action) {
+      Gatherer.remoteGreenstoneServer.progress_bar.setAction(action);
+    }*/
+    
+    static class ActionCancelledException
+	extends Exception
+    {
+    }
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    DELETE COLLECTION
+     * --------------------------------------------------------------------------------------------
+     */
+    static class DeleteCollectionAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+
+	public DeleteCollectionAction(String collection_name)
+	{
+	    this.collection_name = collection_name;
+	}
+
+	public void perform()
+	    throws Exception
+	{
+	    progress_bar.setAction("Deleting collection " + collection_name + "...");
+
+	    String delete_collection_command = "cmd=delete-collection";
+	    delete_collection_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    action_output = remote.sendCommandToServer(delete_collection_command, null);
+	}
+    }
+
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    DELETE COLLECTION FILE
+     * --------------------------------------------------------------------------------------------
+     */
+    static class DeleteCollectionFileAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+	private File collection_file;
+
+	public DeleteCollectionFileAction(String collection_name, File collection_file)
+	{
+	    this.collection_name = collection_name;
+	    this.collection_file = collection_file;
+	}
+
+	public void perform()
+	    throws Exception
+	{
+	    String collection_directory_path = CollectionManager.getCollectionDirectoryPath(collection_name);
+	    String collection_file_relative_path = remote.getPathRelativeToDirectory(collection_file, collection_directory_path);
+	    collection_file_relative_path = collection_file_relative_path.replaceAll((Utility.isWindows() ? "\\\\" : "\\/"), "|");
+	    progress_bar.setAction("Deleting collection file " + collection_file_relative_path + "...");
+
+	    String delete_collection_file_command = "cmd=delete-collection-file";
+	    delete_collection_file_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    delete_collection_file_command += "&file=" + Base64.encodeBytesInSingleLine(collection_file_relative_path.getBytes());
+	    action_output = remote.sendCommandToServer(delete_collection_file_command, null);
+	}
+    }
+
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    DOWNLOAD COLLECTION
+     * --------------------------------------------------------------------------------------------
+     */
+    static class DownloadCollectionAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+
+	public DownloadCollectionAction(String collection_name)
+	{
+	    this.collection_name = collection_name;
+	}
+
+	public void perform()
+	    throws Exception
+	{
+	    progress_bar.setAction("Downloading remote collection " + collection_name + "...");
+
+	    String download_collection_command = "cmd=download-collection";
+	    download_collection_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    download_collection_command += "&lr=" + remote.lang_region;
+	    String zip_file_path = Gatherer.getCollectDirectoryPath() + collection_name + ".zip";
+	    action_output = remote.downloadFile(download_collection_command, zip_file_path);
+
+	    // Delete the existing (local) collection directory
+	    Utility.delete(new File(CollectionManager.getCollectionDirectoryPath(collection_name)));
+
+	    // Unzip the collection just downloaded
+	    UnzipTools.unzipFile(zip_file_path, Gatherer.getCollectDirectoryPath());
+	}
+    }
+
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    DOWNLOAD COLLECTION ARCHIVES
+     * --------------------------------------------------------------------------------------------
+     */
+    static class DownloadCollectionArchivesAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+
+	public DownloadCollectionArchivesAction(String collection_name)
+	{
+	    this.collection_name = collection_name;
+	}
+
+	public void perform()
+	    throws Exception
+	{
+	    progress_bar.setAction("Downloading collection archives for " + collection_name + "...");
+
+	    String download_collection_archives_command = "cmd=download-collection-archives";
+	    download_collection_archives_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    download_collection_archives_command += "&lr=" + remote.lang_region;
+	    String zip_file_path = Gatherer.getCollectDirectoryPath() + collection_name + "-archives.zip";
+	    action_output = remote.downloadFile(download_collection_archives_command, zip_file_path);
+
+	    // Delete the existing (local) collection archives
+	    Utility.delete(new File(CollectionManager.getLoadedCollectionArchivesDirectoryPath()));
+
+	    // Unzip the collection archives just downloaded
+	    UnzipTools.unzipFile(zip_file_path, Gatherer.getCollectDirectoryPath());
+	}
+    }
+
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    DOWNLOAD COLLECTION CONFIGURATIONS
+     * --------------------------------------------------------------------------------------------
+     */
+    static class DownloadCollectionConfigurationsAction
+	extends RemoteGreenstoneServerAction
+    {
+	public DownloadCollectionConfigurationsAction()
+	{
+	}
+
+	public void perform()
+	    throws Exception
+	{
+	    progress_bar.setAction("Downloading collection configurations...");
+
+	    // Delete the existing (local) collect directory
+	    Utility.delete(new File(Gatherer.getCollectDirectoryPath()));
+	    new File(Gatherer.getCollectDirectoryPath()).mkdirs();
+
+	    String download_collection_configurations_command = "cmd=download-collection-configurations";
+	    download_collection_configurations_command += "&lr=" + remote.lang_region;
+	    String zip_file_path = Gatherer.getCollectDirectoryPath() + "collections.zip";
+	    action_output = remote.downloadFile(download_collection_configurations_command, zip_file_path);
+
+	    // Unzip the collection configurations just downloaded
+	    UnzipTools.unzipFile(zip_file_path, Gatherer.getCollectDirectoryPath());
+	}
+    }
+
+     /**
+     * --------------------------------------------------------------------------------------------
+     *    DISCOVERING WHAT VERSION THE REMOTE GREENSTONE SERVER IS (2 or 3)
+     * --------------------------------------------------------------------------------------------
+     */
+
+    static class VersionAction
+	extends RemoteGreenstoneServerAction
+    {
+	public void perform()
+	    throws Exception
+	{
+	    action_output = remote.sendCommandToServer("cmd=greenstone-server-version", null);
+	}	
+    }
+
+    static class LibraryURLSuffixAction
+	extends RemoteGreenstoneServerAction
+    {
+	public void perform()
+	    throws Exception
+	{
+	    action_output = remote.sendCommandToServer("cmd=get-library-url-suffix", null);
+	}	
+    }
+    
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    CHECKING IF A FILE/FOLDER EXISTS ON SERVER SIDE
+     * --------------------------------------------------------------------------------------------
+     */
+    static class ExistsAction 
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+	private File collection_file;
+	
+	public ExistsAction(String collection_name, File collection_file)
+	{
+	    this.collection_name = collection_name;
+	    this.collection_file = collection_file;
+	}
+	
+	public void perform()
+	    throws Exception
+	{
+	    String collection_directory_path = CollectionManager.getCollectionDirectoryPath(collection_name);
+	    String collection_file_relative_path = remote.getPathRelativeToDirectory(collection_file, collection_directory_path);
+	    collection_file_relative_path = collection_file_relative_path.replaceAll((Utility.isWindows() ? "\\\\" : "\\/"), "|");
+	    File file = new File(collection_directory_path, collection_file_relative_path);
+
+	    String file_exists_command = "cmd=file-exists";
+	    file_exists_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    // base64 encode the filename to preserve special characters
+	    file_exists_command += "&file=" + Base64.encodeBytesInSingleLine(collection_file_relative_path.getBytes());
+
+	    // returns either "File <filename> exists" or "File <filename> does not exist"
+	    // for the file/folder collection_file
+	    action_output = remote.sendCommandToServer(file_exists_command, null);
+	}	
+    }
+    
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    DOWNLOAD COLLECTION FILE
+     * --------------------------------------------------------------------------------------------
+     */
+    static class DownloadCollectionFileAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+	private File collection_file;
+
+	public DownloadCollectionFileAction(String collection_name, File collection_file)
+	{
+	    this.collection_name = collection_name;
+	    this.collection_file = collection_file;
+	}
+
+	public void perform()
+	    throws Exception
+	{
+	    String collection_directory_path = CollectionManager.getCollectionDirectoryPath(collection_name);
+	    String collection_file_relative_path = remote.getPathRelativeToDirectory(collection_file, collection_directory_path);
+	    collection_file_relative_path = collection_file_relative_path.replaceAll((Utility.isWindows() ? "\\\\" : "\\/"), "|");
+	    progress_bar.setAction("Downloading collection file " + collection_file_relative_path + "...");
+
+	    String download_collection_file_command = "cmd=download-collection-file";
+	    download_collection_file_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    download_collection_file_command += "&file=" + URLEncoder.encode(collection_file_relative_path, "UTF-8");
+	    download_collection_file_command += "&lr=" + remote.lang_region;
+
+	    // String zip_file_name = collection_name + "-" + collection_file.getName() + ".zip";
+	    // String zip_file_path = collection_directory_path + zip_file_name;	    
+	    String zip_file_path = Gatherer.getCollectDirectoryPath() + collection_name + "-" + collection_file.getName() + ".zip"; // collect/(colgroup/)coltail/colfile.zip
+	    action_output = remote.downloadFile(download_collection_file_command, zip_file_path);
+
+	    // Unzip the collection file just downloaded
+	    UnzipTools.unzipFile(zip_file_path, collection_directory_path);
+	}
+    }
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    DOWNLOAD web.xml FILE --for a remote GS3
+     * --------------------------------------------------------------------------------------------
+     */
+    static class DownloadWebXMLFileAction
+	extends RemoteGreenstoneServerAction
+    {
+	public DownloadWebXMLFileAction()
+	{}
+
+	public void perform()
+	    throws Exception
+	{
+	    String web_xml_directory_path=(Configuration.gli_user_directory_path);
+	    String download_web_xml_file_command = "cmd=download-web-xml-file";
+	    download_web_xml_file_command += "&file=" + URLEncoder.encode("web.xml", "UTF-8");
+	    download_web_xml_file_command += "&lr=" + remote.lang_region;
+	    String zip_file_name = "web-xml.zip";
+	    String zip_file_path = web_xml_directory_path + zip_file_name;
+	    action_output = remote.downloadFile(download_web_xml_file_command, zip_file_path);
+
+	    // Unzip the web.xml file just downloaded
+	    UnzipTools.unzipFile(zip_file_path,web_xml_directory_path);
+	}
+    }
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    GET SCRIPT OPTIONS
+     * --------------------------------------------------------------------------------------------
+     */
+    static class GetScriptOptionsAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String script_name;
+	private String script_arguments;
+
+	public GetScriptOptionsAction(String script_name, String script_arguments)
+	{
+	    this.script_name = script_name;
+	    this.script_arguments = script_arguments;
+		// classinfo.pl script  has a "collection" argument. Convert any colgroup/coltail to colgroup|coltail:
+		// (Note that this may be the only method here that does not url encode the collection name before sendit it to the server)
+		this.script_arguments = script_arguments.replace(File.separatorChar, '|'); 
+	}
+
+	public void perform()
+	    throws Exception
+	{
+	    progress_bar.setAction("Getting options for " + script_name + "...");
+
+	    String get_script_options_command = "cmd=get-script-options";
+	    get_script_options_command += "&script=" + script_name;
+	    get_script_options_command += "&xml=";
+	    get_script_options_command += "&language=" + Configuration.getLanguage();
+	    get_script_options_command += script_arguments;
+	    action_output = remote.sendCommandToServer(get_script_options_command, null);
+	}
+    }
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    GET ALL NAMES OF SITES // for a remote GS3 
+     * --------------------------------------------------------------------------------------------
+     */
+    static class GetSiteNamesAction
+	extends RemoteGreenstoneServerAction
+    {
+	public GetSiteNamesAction()
+	{}
+
+	public void perform()
+	    throws Exception
+	{
+	    progress_bar.setAction("Getting names of sites ...");
+
+	    String get_script_options_command = "cmd=get-site-names";
+	    action_output = remote.sendCommandToServer(get_script_options_command, null);
+	}
+    }
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    MOVE COLLECTION FILE
+     * --------------------------------------------------------------------------------------------
+     */
+    static class MoveCollectionFileAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+	private File source_collection_file;
+	private File target_collection_file;
+
+	public MoveCollectionFileAction(String collection_name, File source_collection_file, File target_collection_file)
+	{
+	    this.collection_name = collection_name;
+	    this.source_collection_file = source_collection_file;
+	    this.target_collection_file = target_collection_file;
+	}
+
+	public void perform()
+	    throws Exception
+	{
+	    String collection_directory_path = CollectionManager.getCollectionDirectoryPath(collection_name);
+	    String source_collection_file_relative_path = remote.getPathRelativeToDirectory(
+                                                          source_collection_file, collection_directory_path);
+	    source_collection_file_relative_path = source_collection_file_relative_path.replaceAll((Utility.isWindows() ? "\\\\" : "\\/"), "|");
+	    String target_collection_file_relative_path = remote.getPathRelativeToDirectory(
+                                                          target_collection_file, collection_directory_path);
+	    target_collection_file_relative_path = target_collection_file_relative_path.replaceAll((Utility.isWindows() ? "\\\\" : "\\/"), "|");
+	    progress_bar.setAction("Moving file " + source_collection_file_relative_path + " -> " + target_collection_file_relative_path + "...");
+
+	    String move_collection_file_command = "cmd=move-collection-file";
+	    move_collection_file_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    move_collection_file_command += "&source=" + Base64.encodeBytesInSingleLine(source_collection_file_relative_path.getBytes());
+	    move_collection_file_command += "&target=" + Base64.encodeBytesInSingleLine(target_collection_file_relative_path.getBytes());
+	    //move_collection_file_command += "&source=" + URLEncoder.encode(source_collection_file_relative_path, "UTF-8");
+	    //move_collection_file_command += "&target=" + URLEncoder.encode(target_collection_file_relative_path, "UTF-8");
+	    action_output = remote.sendCommandToServer(move_collection_file_command, null);
+	}
+    }
+
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    NEW COLLECTION DIRECTORY
+     * --------------------------------------------------------------------------------------------
+     */
+    static class NewCollectionDirectoryAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+	private File new_collection_directory;
+
+	public NewCollectionDirectoryAction(String collection_name, File new_collection_directory)
+	{
+	    this.collection_name = collection_name;
+	    this.new_collection_directory = new_collection_directory;
+	}
+
+	public void perform()
+	    throws Exception
+	{
+	    String collection_directory_path = CollectionManager.getCollectionDirectoryPath(collection_name);
+	    String new_collection_directory_relative_path = remote.getPathRelativeToDirectory(
+                                                            new_collection_directory, collection_directory_path);
+	    new_collection_directory_relative_path = new_collection_directory_relative_path.replaceAll((Utility.isWindows() ? "\\\\" : "\\/"), "|");
+	    progress_bar.setAction("Creating new directory " + new_collection_directory_relative_path + "...");
+
+	    String new_collection_directory_command = "cmd=new-collection-directory";
+	    new_collection_directory_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    new_collection_directory_command += "&directory=" + URLEncoder.encode(new_collection_directory_relative_path, "UTF-8");
+	    action_output = remote.sendCommandToServer(new_collection_directory_command, null);
+	}
+    }
+
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    RUN SCRIPT
+     * --------------------------------------------------------------------------------------------
+     */
+    static class RunScriptAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+	private String script_name;
+	private String script_arguments;
+	private GShell shell;
+
+	public RunScriptAction(String collection_name, String script_name, String script_arguments, GShell shell)
+	{
+	    this.collection_name = collection_name;
+	    this.script_name = script_name;
+	    this.script_arguments = script_arguments;
+	    this.shell = shell;
+	}
+
+	public void perform()
+	    throws Exception
+	{
+	    progress_bar.setAction("Running " + script_name + "...");
+
+	    String run_script_command = "cmd=run-script";
+	    run_script_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    run_script_command += "&script=" + script_name;
+	    run_script_command += "&language=" + Configuration.getLanguage();
+	    run_script_command += script_arguments;
+	    action_output = remote.sendCommandToServer(run_script_command, shell);
+	}
+    }
+
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    UPLOAD COLLECTION FILE
+     * --------------------------------------------------------------------------------------------
+     */
+    static class UploadCollectionFilesAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+	private File[] collection_files;
+
+
+	public UploadCollectionFilesAction(String collection_name, File[] collection_files)
+	{
+	    this.collection_name = collection_name;
+	    this.collection_files = collection_files;
+	}
+
+
+	public void perform()
+	    throws Exception
+	{
+	    progress_bar.setAction("Uploading collection files...");
+
+	    // Determine the file paths relative to the collection directory
+	    String collection_directory_path = CollectionManager.getCollectionDirectoryPath(collection_name);
+	    String[] collection_file_relative_paths = new String[collection_files.length];
+	    for (int i = 0; i < collection_files.length; i++) {
+		collection_file_relative_paths[i] = remote.getPathRelativeToDirectory(collection_files[i], collection_directory_path);
+	    }
+
+	    // Zip up the files to send to the server
+	    //String zip_file_name = collection_name + "-" + System.currentTimeMillis() + ".zip";
+            //String zip_file_path = collection_directory_path + zip_file_name;
+	    String zip_file_path = collection_directory_path; // collect/(colgroup/)coltail/
+	    String zip_file_name = new File(zip_file_path).getName() + "-" + System.currentTimeMillis() + ".zip"; // <collection_tail_name>-<time>.zip
+	    zip_file_path += zip_file_name; // collect/(colgroup/)coltail/coltail-time.zip
+	    ZipTools.zipFiles(zip_file_path, collection_directory_path, collection_file_relative_paths);
+	    
+	    // Upload the zip file
+	    String upload_collection_file_command = "cmd=upload-collection-file";
+	    upload_collection_file_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    upload_collection_file_command += "&file=" + URLEncoder.encode(zip_file_name, "UTF-8");
+	    upload_collection_file_command += "&directory=";
+	    upload_collection_file_command += "&zip=true";
+	    upload_collection_file_command += "&lr=" + remote.lang_region;
+	    action_output = remote.uploadFile(upload_collection_file_command, zip_file_path);
+	}
+    }
+
+
+    /**
+     * --------------------------------------------------------------------------------------------
+     *    UPLOAD FILES INTO COLLECTION
+     * --------------------------------------------------------------------------------------------
+     */
+    static class UploadFilesIntoCollectionAction
+	extends RemoteGreenstoneServerAction
+    {
+	private String collection_name;
+	private File[] source_files;
+	private File target_collection_directory;
+
+
+	public UploadFilesIntoCollectionAction(String collection_name, File[] source_files, File target_collection_directory)
+	{
+	    this.collection_name = collection_name;
+	    this.source_files = source_files;
+	    this.target_collection_directory = target_collection_directory;
+	}
+
+
+	public void perform()
+	    throws Exception
+	{
+	    String collection_directory_path = CollectionManager.getCollectionDirectoryPath(collection_name);
+	    String target_collection_directory_relative_path = remote.getPathRelativeToDirectory(
+                                                               target_collection_directory, collection_directory_path);
+	    target_collection_directory_relative_path = target_collection_directory_relative_path.replaceAll((Utility.isWindows() ? "\\\\" : "\\/"), "|");
+	    progress_bar.setAction("Uploading files into collection...");
+	   
+	    //String zip_file_name = collection_name + "-" + System.currentTimeMillis() + ".zip";
+	    //String zip_file_path = Gatherer.getCollectDirectoryPath() + zip_file_name;
+	    String zip_file_path = Gatherer.getCollectDirectoryPath() 
+		+ collection_name + "-" + System.currentTimeMillis() + ".zip"; // "collect/(colgroup/)collection_tail_name-<time>.zip"
+	    String zip_file_name = new File(zip_file_path).getName(); // "collection_tail_name-<time>.zip"
+	    DebugStream.println("Zip file path: " + zip_file_path);
+
+	    String base_directory_path = source_files[0].getParentFile().getAbsolutePath();
+	    DebugStream.println("Base directory path: " + base_directory_path);
+	    String[] source_file_relative_paths = new String[source_files.length];
+	    for (int i = 0; i < source_files.length; i++) {
+		DebugStream.println("Source file path: " + source_files[i]);
+		source_file_relative_paths[i] = remote.getPathRelativeToDirectory(source_files[i], base_directory_path);
+	    }
+	    
+	    ZipTools.zipFiles(zip_file_path, base_directory_path, source_file_relative_paths);
+
+	    String upload_collection_file_command = "cmd=upload-collection-file";
+	    upload_collection_file_command += "&c=" + URLEncoder.encode(collection_name.replace(File.separatorChar, '|'), "UTF-8");
+	    upload_collection_file_command += "&file=" + URLEncoder.encode(zip_file_name, "UTF-8");
+	    upload_collection_file_command += "&directory=" + URLEncoder.encode(target_collection_directory_relative_path, "UTF-8");
+	    upload_collection_file_command += "&zip=true";
+	    upload_collection_file_command += "&lr=" + remote.lang_region;
+	    action_output = remote.uploadFile(upload_collection_file_command, zip_file_path);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/Unzip.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/Unzip.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/Unzip.java	(revision 31635)
@@ -0,0 +1,119 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: David Bainbridge, NZDL Project, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.remote;
+
+import java.io.*;
+import java.util.*;
+import java.util.zip.*;
+
+
+public class Unzip 
+{
+
+    public static final void copyInputStream(InputStream in, OutputStream out)
+	throws IOException
+    {
+	byte[] buffer = new byte[1024];
+	int len;
+
+	while((len = in.read(buffer)) >= 0)
+	    out.write(buffer, 0, len);
+
+	in.close();
+	out.close();
+    }
+
+
+    static public void main(String[] args) 
+    {
+	if (args.length != 2) {
+	    System.err.println("Usage: Unzip <zip-file> <destination-directory>");
+	    return;
+	}
+
+	String zip_file_path = args[0];
+	// System.err.println("Zip file path: " + zip_file_path);
+	String destination_directory_path = args[1];
+	if (!destination_directory_path.endsWith(File.separator)) {
+	    destination_directory_path += File.separator;
+	}
+	// System.err.println("Destination directory path: " + destination_directory_path);
+
+	try {
+	    ZipFile zipFile = new ZipFile(zip_file_path);
+
+	    Enumeration entries = zipFile.entries();
+	    while (entries.hasMoreElements()) {
+		ZipEntry entry = (ZipEntry) entries.nextElement();
+
+		if (entry.isDirectory()) {
+		    // Assume directories are stored parents first then children.
+		    // System.err.println("Extracting directory: " + entry.getName());
+		    // This is not robust, just for demonstration purposes.
+		    String dir_name = entry.getName();
+		    String full_dir_name = destination_directory_path + dir_name;
+		    // System.err.println("Full directory name: " + full_dir_name);
+		    File dirFile = new File(full_dir_name);
+		    dirFile.mkdir();
+		}
+		else {
+		    // System.err.println("Extracting file: " + entry.getName());
+		    String file_name = entry.getName();
+		    String full_file_name = destination_directory_path + file_name;
+		    File directory = (new File(full_file_name)).getParentFile();
+		    // System.err.println("Full file name: " + full_file_name);
+		    if (!directory.exists() && !directory.mkdirs()) {
+			System.err.println("Error: Could not create directory " + directory + "!");
+		    }
+
+		    FileOutputStream fos = new FileOutputStream(full_file_name);
+		    BufferedOutputStream bos = new BufferedOutputStream(fos);
+
+		    copyInputStream(zipFile.getInputStream(entry),bos);
+		}
+	    }
+
+	    zipFile.close();
+	}
+	catch (Exception exception) {
+	    System.out.println("ERROR: " + exception);
+	    exception.printStackTrace();
+	    System.exit(1);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipCollectionArchives.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipCollectionArchives.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipCollectionArchives.java	(revision 31635)
@@ -0,0 +1,117 @@
+package org.greenstone.gatherer.remote;
+
+import java.io.*;
+import java.util.zip.*;
+
+
+/**
+ */
+public class ZipCollectionArchives
+{
+    static public void main(String[] args)
+    {
+	if (args.length != 3) {
+	    System.err.println("Usage: ZipCollectionArchives <zip-file> <collect-directory-path> <collection-name>");
+	    return;
+	}
+
+	String zip_file_path = args[0];
+	String collect_directory_path = args[1];
+	String collection_name = args[2];
+
+	if (!collect_directory_path.endsWith(File.separator)) {
+	    collect_directory_path += File.separator;
+	}
+
+ 	try {
+	    ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip_file_path));
+
+	    // Add archives directory, with doc.xml files only
+	    String archives_relative_path = collection_name + File.separator + "archives";
+	    ZipTools.ZipFilter collection_archives_zip_filter = new CollectionArchivesZipFilter();
+	    ZipTools.addFileToZip(zos, collect_directory_path, archives_relative_path, collection_archives_zip_filter);
+
+	    zos.close();
+	}
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	}
+    }
+
+
+    static public class CollectionArchivesZipFilter
+	extends ZipTools.NullZipFilter
+    {
+	private byte[] bytes_remaining = new byte[0];
+	private int section_depth = 0;
+	private boolean in_section_content = false;
+
+
+	public boolean shouldIncludeFile(String relative_file_path)
+	{
+	    // Only doc.xml files are strictly required, but we include archives.inf as well to prevent
+	    //   errors when zipping up the archives of a collection where no files were imported
+	    //   (in this case the zip file would contain nothing at all)
+	    return (relative_file_path.equals("archives.inf") || relative_file_path.endsWith(File.separator + "archives.inf") || relative_file_path.equals("doc.xml") || relative_file_path.endsWith(File.separator + "doc.xml"));
+	}
+
+
+	public boolean shouldIncludeFileContent(String relative_file_path)
+	{
+	    // Only content for doc.xml files is included
+	    return (relative_file_path.equals("doc.xml") || relative_file_path.endsWith(File.separator + "doc.xml"));
+	}
+
+
+	public void filterFileContent(String relative_file_path, BufferedInputStream bis, ZipOutputStream zos)
+	{
+	    // Reset the status in case there were errors in previous doc.xml files
+	    section_depth = 0;
+	    in_section_content = false;
+
+	    // Filter out the <Content>...</Content> of the doc.xml files
+	    try {
+		BufferedReader reader = new BufferedReader(new InputStreamReader(bis, "UTF-8"));
+		String line = null;
+		while ((line = reader.readLine()) != null) {
+		    // If this line isn't filtered, write it out to zos
+		    if (!filterFileLine(line)) {
+			line += "\n";
+			byte[] bytes = line.getBytes("UTF-8");
+			zos.write(bytes, 0, bytes.length);
+		    }
+		}
+	    }
+	    catch (Exception exception) {
+		exception.printStackTrace();
+	    }
+	}
+
+
+	private boolean filterFileLine(String line)
+	{
+	    boolean filter_line = false;
+
+	    if (line.indexOf("<Section>") != -1) {
+		section_depth++;
+	    }
+	    if (line.indexOf("<Content>") != -1) {
+		in_section_content = true;
+	    }
+
+	    // If we're in a subsection or in a content element, filter this line
+	    if (section_depth > 1 || in_section_content) {
+		filter_line = true;
+	    }
+
+	    if (line.indexOf("</Content>") != -1) {
+		in_section_content = false;
+	    }
+	    if (line.indexOf("</Section>") != -1) {
+		section_depth--;
+	    }
+
+	    return filter_line;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipCollectionConfigurations.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipCollectionConfigurations.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipCollectionConfigurations.java	(revision 31635)
@@ -0,0 +1,155 @@
+package org.greenstone.gatherer.remote;
+
+import java.io.*;
+import java.util.zip.*;
+
+
+/**
+ */
+public class ZipCollectionConfigurations
+{
+    static public void main(String[] args)
+    {
+	if (args.length != 4) {
+	    System.err.println("Usage: ZipCollectionConfigurations <zip-file> <collect-directory-path> <user-name> <user-groups>");
+	    return;
+	}
+
+	String zip_file_path = args[0];
+	String collect_directory_path = args[1];
+	String user_name = args[2];
+	String user_groups = args[3];
+
+	if (!collect_directory_path.endsWith(File.separator)) {
+	    collect_directory_path += File.separator;
+	}
+	File collect_directory = new File(collect_directory_path);
+
+ 	try {
+	    ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip_file_path));
+	    ZipTools.ZipFilter collection_etc_zip_filter = new CollectionEtcZipFilter();
+	    ZipTools.ZipFilter collection_metadata_zip_filter = new CollectionMetadataZipFilter();
+
+	    addUserAccessibleCollections(collect_directory_path, "", collect_directory, zos, 
+					 collection_etc_zip_filter, collection_metadata_zip_filter,
+					 user_name, user_groups);
+
+	    
+
+	    zos.putNextEntry(new ZipEntry(".gli"));
+	    zos.close();
+	}
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	}
+    }
+
+    static private void addUserAccessibleCollections(String toplevel_path,        // remains unchanged
+						     String relative_path,        // grows with each level of recursion
+						     File collect_directory,      // current collection (group) directory being considered
+						     ZipOutputStream zos, 
+						     ZipTools.ZipFilter collection_etc_zip_filter,
+						     ZipTools.ZipFilter collection_metadata_zip_filter,
+						     String user_name,
+						     String user_groups) 
+    {
+	
+	if(!relative_path.equals("")) {
+	    relative_path += File.separator;
+	}
+	
+	// Add the etc and metadata directories from each of the collections the user has access to
+	File[] collection_directories = collect_directory.listFiles();
+	for (int i = 0; i < collection_directories.length; i++) {
+	    if (collection_directories[i].isDirectory()) {
+		String collection_name = collection_directories[i].getName();
+		if (checkAccess(collection_name, user_name, user_groups)) {
+		    
+		    String etc_relative_path = relative_path + collection_directories[i].getName() + File.separator + "etc";
+		    ZipTools.addFileToZip(zos, toplevel_path, etc_relative_path, collection_etc_zip_filter);
+		    
+		    // The "metadata" directory may not exist (non-GLI collections)
+		    String metadata_relative_path = relative_path + collection_directories[i].getName() + File.separator + "metadata";
+		    File metadata_directory = new File(toplevel_path + File.separator + metadata_relative_path);
+		    if (metadata_directory.exists()) {
+			ZipTools.addFileToZip(zos, toplevel_path, metadata_relative_path, collection_metadata_zip_filter);
+		    } 
+		    else { // if a collection contains no metadata directory, then check if it is a collectgroup
+			
+			// read from collection config file in etc folder
+			File config_file = new File(collection_directories[i], "etc"+File.separator+"collect.cfg");
+			if(config_file.exists()) {
+			    try {
+				BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(config_file)));
+				String line = null;
+				while((line = reader.readLine()) != null) { // read lines and look for: collectgroup true
+				    line = line.trim(); // need to ignore surrounding whitespace
+				    if(line.startsWith("collectgroup") && line.endsWith("true")) {
+					// if collectgroup, then recurse on current folder
+					addUserAccessibleCollections(toplevel_path, relative_path+collection_directories[i].getName(), collection_directories[i],
+								     zos, collection_etc_zip_filter, collection_metadata_zip_filter,
+								     user_name, user_groups);
+				    }
+				}
+				
+				reader.close();
+			    } catch(Exception e) {
+				System.err.println("Exception reading from file " + config_file + ": " + e);
+				e.printStackTrace();
+			    }
+			}			    
+		    }
+		}
+	    }
+	}
+    }
+
+
+    static private boolean checkAccess(String collection_name, String user_name, String user_groups)
+    {
+	String[] user_groups_array = user_groups.split(",");
+	for (int i = 0; i < user_groups_array.length; i++) {
+	    if (user_groups_array[i].equals("all-collections-editor")) {
+		return true;
+	    }
+	    if (user_groups_array[i].equals("personal-collections-editor") && collection_name.startsWith(user_name + "-")) {
+		return true;
+	    }
+	    if (user_groups_array[i].equals(collection_name + "-collection-editor")) {
+		return true;
+	    }
+	}
+
+	return false;
+    }
+
+
+    static private class CollectionEtcZipFilter
+	extends ZipTools.NullZipFilter
+    {
+	public boolean shouldIncludeFile(String relative_file_path)
+	{
+	    // Only collect.cfg files are included
+	    if (relative_file_path.endsWith("collect.cfg")){
+		return (relative_file_path.equals("collect.cfg") || relative_file_path.endsWith(File.separator + "collect.cfg"));
+	    }
+
+	    // Only collectConfig.xml files are included
+	    if (relative_file_path.endsWith("collectionConfig.xml")){
+		return (relative_file_path.equals("collectionConfig.xml") || relative_file_path.endsWith(File.separator + "collectionConfig.xml"));
+	    }
+	    return false;
+	}
+    }
+
+
+    static private class CollectionMetadataZipFilter
+	extends ZipTools.NullZipFilter
+    {
+	public boolean shouldIncludeFileContent(String relative_file_path)
+	{
+	    // We don't include content for any of the metadata set files
+	    return false;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipCollectionShell.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipCollectionShell.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipCollectionShell.java	(revision 31635)
@@ -0,0 +1,86 @@
+package org.greenstone.gatherer.remote;
+
+import java.io.*;
+import java.util.zip.*;
+
+
+/**
+ */
+public class ZipCollectionShell
+{
+    static public void main(String[] args)
+    {
+	if ((args.length < 3) || (args.length > 4)) {
+	    System.err.println("Usage: ZipCollectionShell <zip-file> <collect-directory-path> <collection-name> [gsdl3]");
+	    return;
+	}
+
+	String zip_file_path = args[0];
+	String collect_directory_path = args[1];
+	String collection_name = args[2];
+	String run_gsdl3 ="";
+	if (args.length == 4){
+	    run_gsdl3 = args[3];
+	}
+	if (!collect_directory_path.endsWith(File.separator)) {
+	    collect_directory_path += File.separator;
+	}
+
+ 	try {
+	    ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip_file_path));
+	    ZipTools.ZipFilter null_zip_filter = new ZipTools.NullZipFilter();
+
+	    // Add archives directory, with doc.xml files only
+	    String archives_relative_path = collection_name + File.separator + "archives";
+	    ZipTools.ZipFilter collection_archives_zip_filter = new ZipCollectionArchives.CollectionArchivesZipFilter();
+	    ZipTools.addFileToZip(zos, collect_directory_path, archives_relative_path, collection_archives_zip_filter);
+
+	    // Add /etc/collect.cfg file for 2 GS2
+	    if (run_gsdl3.length()==0){
+		String collect_cfg_relative_path = collection_name + File.separator + "etc" + File.separator + "collect.cfg";
+		ZipTools.addFileToZip(zos, collect_directory_path, collect_cfg_relative_path, null_zip_filter);
+	    }
+	    // Add /etc/collectConfig.xml file for a GS3
+	    if (run_gsdl3.equals("gsdl3")){
+		String collectionConfig_xml_relative_path = collection_name + File.separator + "etc" + File.separator + "collectionConfig.xml";
+		ZipTools.addFileToZip(zos, collect_directory_path, collectionConfig_xml_relative_path, null_zip_filter);
+	    }
+	    // Add images directory
+	    String images_relative_path = collection_name + File.separator + "images";
+	    ZipTools.addFileToZip(zos, collect_directory_path, images_relative_path, null_zip_filter);
+
+	    // Add import directory, with zero-length files except for metadata.xml
+	    String import_relative_path = collection_name + File.separator + "import";
+	    ZipTools.ZipFilter collection_import_zip_filter = new CollectionImportZipFilter();
+	    ZipTools.addFileToZip(zos, collect_directory_path, import_relative_path, collection_import_zip_filter);
+
+	    // Add index/build.cfg file
+	    String build_cfg_relative_path = collection_name + File.separator + "index" + File.separator + "build.cfg";
+	    ZipTools.addFileToZip(zos, collect_directory_path, build_cfg_relative_path, null_zip_filter);
+
+	    // Add macros/extra.dm file
+	    String extra_dm_relative_path = collection_name + File.separator + "macros" + File.separator + "extra.dm";
+	    ZipTools.addFileToZip(zos, collect_directory_path, extra_dm_relative_path, null_zip_filter);
+
+	    // Add metadata directory
+	    String metadata_relative_path = collection_name + File.separator + "metadata";
+	    ZipTools.addFileToZip(zos, collect_directory_path, metadata_relative_path, null_zip_filter);
+
+	    zos.close();
+	}
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	}
+    }
+
+
+    static private class CollectionImportZipFilter
+	extends ZipTools.NullZipFilter
+    {
+	public boolean shouldIncludeFileContent(String relative_file_path)
+	{
+	    // We only include file content for metadata.xml files
+	    return (relative_file_path.equals("metadata.xml") || relative_file_path.endsWith(File.separator + "metadata.xml"));
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipFiles.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipFiles.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipFiles.java	(revision 31635)
@@ -0,0 +1,28 @@
+package org.greenstone.gatherer.remote;
+
+import java.io.File;
+import java.util.ArrayList;
+
+
+/**
+ */
+public class ZipFiles
+{
+    static public void main(String[] args)
+    {
+	if (args.length < 3) {
+	    System.err.println("Usage: ZipFiles <zip-file> <base-directory-path> (<files> ...)");
+	    return;
+	}
+
+	String zip_file_path = args[0];
+	String base_directory_path = args[1];
+	ArrayList relative_file_paths_list = new ArrayList();
+	for (int i = 2; i < args.length; i++) {
+	    relative_file_paths_list.add(args[i]);
+	}
+
+	String[] relative_file_paths = (String[]) relative_file_paths_list.toArray(new String[0]);
+	ZipTools.zipFiles(zip_file_path, base_directory_path, relative_file_paths);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipTools.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipTools.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/remote/ZipTools.java	(revision 31635)
@@ -0,0 +1,151 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: David Bainbridge, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.remote;
+
+
+import java.io.*;
+import java.util.zip.*;
+
+
+public class ZipTools
+{
+    static public void zipFiles(String zip_file_path, String base_directory_path, String[] relative_file_paths)
+    {
+	try {
+	    ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip_file_path));
+	    ZipFilter null_zip_filter = new NullZipFilter();
+
+	    // Add each file/directory in turn to the zip file
+	    for (int i = 0; i < relative_file_paths.length; i++) {
+		String relative_file_path = relative_file_paths[i];
+		addFileToZip(zos, base_directory_path, relative_file_path, null_zip_filter);
+	    }
+
+	    zos.close();
+	}
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	}
+    }
+
+
+    static public void addFileToZip(ZipOutputStream zos, String base_directory_path, String relative_file_path, ZipFilter zip_filter)
+    {
+	File file = new File(base_directory_path + File.separator + relative_file_path);
+
+	// Check that the file/directory exists
+	if (!file.exists()) {
+	    System.err.println("File " + file + " does not exist!");
+	    return;
+	}
+
+	try {
+	    // Directory case
+	    if (file.isDirectory()) {
+		// Add a zip entry for this directory
+		// Actually, don't -- this messes things up completely on Windows and doesn't seem to matter on Linux
+		// zos.putNextEntry(new ZipEntry(relative_file_path + File.separator));
+
+		// Apply recursively to each of the children of the directory
+		File[] child_files = file.listFiles();
+		for (int i = 0; i < child_files.length; i++) {
+		    addFileToZip(zos, base_directory_path, relative_file_path + File.separator + child_files[i].getName(), zip_filter);
+		}
+
+		// empty directory. Java zips don't allow empty dirs, so need to put some dummy file in there
+		if(child_files.length == 0) {
+		    String rel_path = relative_file_path.replace('\\', '/') + File.separator + "empty";
+		    System.err.println("Zipping empty dir requires dummy file: " + rel_path);
+		    zos.putNextEntry(new ZipEntry(rel_path));
+		}
+	    }
+
+	    // File case
+	    else {
+		// Add a zip entry for this file
+		if (zip_filter.shouldIncludeFile(relative_file_path)) {
+		    // Always use Unix style paths in zip files, even on Windows
+		    zos.putNextEntry(new ZipEntry(relative_file_path.replace('\\', '/')));
+
+		    if (zip_filter.shouldIncludeFileContent(relative_file_path)) {
+			BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+			zip_filter.filterFileContent(relative_file_path, bis, zos);
+			bis.close();
+		    }
+		}
+	    }
+	}
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	}
+    }
+
+
+    public interface ZipFilter
+    {
+	public boolean shouldIncludeFile(String relative_file_path);
+
+	public boolean shouldIncludeFileContent(String relative_file_path);
+
+	public void filterFileContent(String relative_file_path, BufferedInputStream bis, ZipOutputStream zos);
+    }
+
+
+    static public class NullZipFilter
+	implements ZipFilter
+    {
+	public boolean shouldIncludeFile(String relative_file_path)
+	{
+	    // All files are included
+	    return true;
+	}
+
+
+	public boolean shouldIncludeFileContent(String relative_file_path)
+	{
+	    // Content for all files is included
+	    return true;
+	}
+
+
+	public void filterFileContent(String relative_file_path, BufferedInputStream bis, ZipOutputStream zos)
+	{
+	    // No filtering: just read the file and write it directly out to zip
+	    try {
+		byte[] data = new byte[1024];
+		int bytes_read;
+		while ((bytes_read = bis.read(data, 0, 1024)) > -1) {
+		    zos.write(data, 0, bytes_read);
+		}
+	    }
+	    catch (Exception exception) {
+		// We can't use DebugStream here
+		exception.printStackTrace();
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GBuildProgressMonitor.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GBuildProgressMonitor.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GBuildProgressMonitor.java	(revision 31635)
@@ -0,0 +1,598 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.shell;
+
+import java.awt.Component;
+import java.util.ArrayList;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.gui.GProgressBar;
+import org.greenstone.gatherer.util.StaticStrings;
+/** This implementation of <i>GShellProgressMonitor</i> is designed to parse and translate the progress of a buildcol.pl call.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class GBuildProgressMonitor
+    implements GShellProgressMonitor {
+    /** The minimum value = 0 */
+    static final private int MIN = 0;
+    /** In order to get a smoothish progress bar, set the maximum to a large number = 1000000 */
+    static final private int MAX = 1000000;
+    /** The various states the build progress state machine can be in, starting at the base state. */
+    static final int BASE                          = -1;
+    static final int BUILD                         =  0;
+    static final int COMPRESSTEXT                  = 10;
+    static final int COMPRESSTEXT_COLLECTTEXTSTATS = 11;
+    static final int COMPRESSTEXT_CREATINGCOMPRESS = 12;
+    static final int COMPRESSTEXT_COMPRESSINGTEXT  = 13;
+    static final int INDEX                         = 20;
+    static final int INDEX_CREATINGINDEXDIC        = 21;
+    static final int INDEX_INVERTINGTEXT           = 22;
+    static final int INDEX_CREATETHEWEIGHTS        = 23;
+    static final int INDEX_CREATESTEMMEDDIC        = 24;
+    static final int INDEX_CREATINGSTEMINDX        = 25;
+    static final int CREATEINFODATA                = 30;
+    static final int PHIND                         = 40;
+    static final int PHIND_EXTRACTINGVOCAB         = 41;
+    static final int PHIND_EXTRACTINGPHRASE        = 42;
+    static final int PHIND_SORTANDRENUMBER         = 43;
+    static final int PHIND_PHRASEDATABASES         = 44;
+    static final int PHIND_WORDLEVELINDEXES        = 45;
+    static final int PHIND_DOCINFODATABASES        = 46;
+    static final int CREATINGAUXILARY              = 50;
+    /** Now the language independant sentinel strings. */
+    static final String BADARGUMENT_ELEMENT    = "BadArgument";
+    static final String BADARGUMENTVALUE_ELEMENT = "BadArgumentValue";
+    static final String BADCLASSIFIER_ELEMENT = "BadClassifier";
+    static final String BADPLUGIN_ELEMENT = "BadPlugin";
+    static final String BUILD_ELEMENT          = "Build";
+    static final String COLLECTTEXTSTATS_VALUE = "CollectTextStats";
+    static final String COMPRESSINGTEXT_VALUE  = "CompressingText";
+    static final String COMPRESSTEXT_VALUE     = "CompressText";
+    static final String CREATINGAUXILARY_VALUE = "CreatingAuxilary";
+    static final String CREATINGCOMPRESS_VALUE = "CreatingCompress";
+    static final String CREATINGINDEXDIC_VALUE = "CreatingIndexDic";
+    static final String CREATINGSTEMINDX_VALUE = "CreatingStemIndx";
+    static final String CREATEINFODATA_VALUE   = "CreateInfoData";
+    static final String CREATESTEMMEDDIC_VALUE = "CreateStemmedDic";
+    static final String CREATETHEWEIGHTS_VALUE = "CreateTheWeights";
+    static final String DOCINFODATABASES_VALUE = "DocInfoDatabases";
+    static final String EXTRACTINGPHRASE_VALUE = "ExtractingPhrase";
+    static final String EXTRACTINGVOCAB_VALUE  = "ExtractingVocab";
+    static final String FATALERROR_ELEMENT     = "FatalError";
+    static final String INDEX_VALUE            = "Index";
+    static final String INVERTINGTEXT_VALUE    = "InvertingText";
+    static final String NAME_ATTRIBUTE         = "name";
+    static final String PHASE_ELEMENT          = "Phase";
+    static final String PHIND_VALUE            = "Phind";
+    static final String PHRASEDATABASES_VALUE  = "PhraseDatabases";
+    static final String SKIPCREATINGCOMP_VALUE = "SkipCreatingComp";
+    static final String SORTANDRENUMBER_VALUE  = "SortAndRenumber";
+    static final String SOURCE_ATTRIBUTE       = "source";
+    static final String STAGE_ELEMENT          = "Stage";
+    static final String WARNING_ELEMENT        = "Warning";   
+    static final String WORDLEVELINDEXES_VALUE = "WordLevelIndexes";
+
+    static final private String ARGUMENT_ATTRIBUTE = "a";
+    static final private String CLASSIFIER_ATTRIBUTE = "c";
+    static final private String PLUGIN_ATTRIBUTE = "p";
+
+    /** Indicates if the GUI has asked the process this object monitors to stop. */
+    private boolean stop = false;
+    /** The current number of stages we have completed. */
+    private int current_stages = 0;
+    /** The number of stages we are expecting to complete. */
+    private int expected_stages = 0;
+
+    private int threshold = Configuration.LIBRARIAN_MODE;
+    /** The current state we are in. The state will change depending on the next flag recieved. */
+    private int state = BASE;
+    /** The progress bar this monitor updates. */
+    private GProgressBar progress_bar;
+    /** A progress bar that is shared between this this listener and the Import monitor. */
+    private GProgressBar shared_progress_bar;
+
+    /** Construct a new GBuildProgressMonitor. */
+    public GBuildProgressMonitor(GProgressBar shared_progress_bar) {
+	this.shared_progress_bar = shared_progress_bar;
+	progress_bar = new GProgressBar();
+	progress_bar.setMaximum(MAX);
+	progress_bar.setMinimum(MIN);
+	progress_bar.setString(null);
+	progress_bar.setStringPainted(true);
+	setValue(MIN);
+    }
+
+    /** Method to register a new progress bar with this monitor.
+     * @param progress_bar The new <strong>GProgressBar</strong>.
+     */	 
+    public void addProgressBar(GProgressBar progress_bar) {
+	this.progress_bar = progress_bar;
+	progress_bar.setMaximum(MAX);
+	progress_bar.setMinimum(MIN);
+	progress_bar.setStringPainted(false);
+	progress_bar.setString(null);
+	setValue(MIN);
+    }
+
+    /** Determine the script exit value according to the progress monitor. This gets around a problem where several script failures actually result with a successful exit value.
+     * @return A <i>int</i> with a value of zero if and only if the script was successful.
+     */
+    public int exitValue() {
+	if(state == BASE) {
+	    return 0;
+	}
+	return 1;
+    }
+    /** Method to retrieve whatever control is being used as the progress indicator. Usually a <strong>GProgressBar</strong> but there may be others implemented later.
+     * @return A <strong>Component</strong> on which the progress of the process is being displayed.
+     */
+    public Component getProgress() {
+	return progress_bar;
+    }
+
+    public GProgressBar getSharedProgress() {
+	return shared_progress_bar;
+    }
+
+    public void messageOnProgressBar(String message)
+    {
+	if (message!=null && !message.equals("")) {
+	    progress_bar.setString(message);
+	    shared_progress_bar.setString(message);
+	}
+	else {
+	    progress_bar.setString(null);
+	    shared_progress_bar.setString(null);
+	}
+    }
+
+    /** Method to determine the state of the stop flag, which may be set by the visual component that created this monitor.
+     * @return A <strong>boolean</strong> indicating if the process has been asked to stop.
+     */
+    public synchronized boolean hasSignalledStop() {
+	return stop;
+    }
+    /** Inform the progress bar that it should programatically increment progress by one step. */
+    public void increment() {
+	// There is no reason this should be called for a build.
+    }
+
+    /** This method is used to 'feed in' a line of text captured from the process.
+     * @param queue a queue which at the moment should contain a single GShellEvent
+     */
+    public void process(ArrayList queue) {
+	GShellEvent event = (GShellEvent) queue.get(0);
+	// Remove 'build.pl> ' bit
+	String line = event.getMessage();
+	line = line.substring(line.indexOf(StaticStrings.GREATER_THAN_CHARACTER) + 1);
+	line = line.trim();
+	// We are only really interested in processing pseudo-XML tags
+	if(line.startsWith(StaticStrings.LESS_THAN_CHARACTER) && line.endsWith(StaticStrings.GREATER_THAN_CHARACTER)) {
+	    // All events are vetoed
+	    event.veto();
+	    
+	    // Create a new element from it
+	    GShellElement element = new GShellElement(line);
+	    // Now change states within the state machine as appropriate. A state change may subsequently incur a progress update.
+	    String name = null;
+	    String value = null;
+	    switch(state) {
+	    case BASE:
+		// The only thing we would expect is a build message
+		name = element.getElementName();
+		if(name.equals(BUILD_ELEMENT)) {
+		    progress_bar.setIndeterminate(false);
+		    state = BUILD;
+		    // Produce a lower detail message if required
+		    if(Configuration.getMode() <= threshold && !stop) {
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.BuildBegun1"), event.getStatus()));
+		    }
+		}
+		else {
+		    // Unknown command, go indeterminate
+		    DebugStream.println("Unknown name: " + name);
+		    progress_bar.setIndeterminate(true);
+		}
+		name = null;
+		break;
+	    case BUILD:
+		// The only thing we would expect is a stage element or a 
+		// bad classifier arg name
+		name = element.getElementName();
+
+		if(name.equals(STAGE_ELEMENT)) {
+		    progress_bar.setIndeterminate(false);
+		    // Now we determine what stage we are moving to
+		    value = element.getAttribute(NAME_ATTRIBUTE);
+		    if(value.equals(COMPRESSTEXT_VALUE)) {
+			state = COMPRESSTEXT;
+			if(Configuration.getMode() <= threshold) {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.CompressText"), event.getStatus()));
+			}
+		    }
+		    else if(value.equals(INDEX_VALUE)) {
+			state = INDEX;
+			if(Configuration.getMode() <= threshold) {
+			    String args[] = new String[1];
+			    args[0] = element.getAttribute(SOURCE_ATTRIBUTE);
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.Index", args), event.getStatus()));
+			    args = null;
+			}
+		    }
+		    else if(value.equals(CREATEINFODATA_VALUE)) {
+			state = CREATEINFODATA;
+			if(Configuration.getMode() <= threshold) {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.InfoDatabase"), event.getStatus()));
+			}
+		    }
+		    else if(value.equals(PHIND_VALUE)) {
+			state = PHIND;
+			if(Configuration.getMode() <= threshold) {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.Phind"), event.getStatus()));
+			}
+		    }
+		    else if(value.equals(CREATINGAUXILARY_VALUE)) {
+			state = CREATINGAUXILARY;
+			if(Configuration.getMode() <= threshold) {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.Auxilary"), event.getStatus()));
+			}
+		    }
+		    else {
+			// Unknown command, go indeterminate
+			DebugStream.println("Unknown value: " + value);
+			progress_bar.setIndeterminate(true);
+		    }
+		    value = null;
+		}
+		// The build end message indicates a successfulish build.
+		else if(name.equals(BUILD_ELEMENT) && element.isClosed()) {
+		    progress_bar.setIndeterminate(false);
+		    setValue(MAX);
+		    state = BASE;
+		    if(Configuration.getMode() <= threshold) {
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.BuildComplete1"), event.getStatus()));
+		    }
+		}
+		// We had a bad argument to a plugin/classifier
+		else if (name.equals(BADARGUMENT_ELEMENT)) {
+		    if(Configuration.getMode() <= threshold) {
+			String args[] = new String[1];
+			args[0] = element.getAttribute(ARGUMENT_ATTRIBUTE);    
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadArgument", args), event.getStatus()));
+			args = null;
+		    }
+		}
+		// We had a bad argument value to a plugin/clasifier
+		else if (name.equals(BADARGUMENTVALUE_ELEMENT)) {
+		    if(Configuration.getMode() <= threshold) {
+			String args[] = new String[1];
+			args[0] = element.getAttribute(ARGUMENT_ATTRIBUTE);    
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadArgumentValue", args), event.getStatus()));
+			args = null;
+		    }
+		}
+		// And this one tellsa us the plugin
+		else if (name.equals(BADPLUGIN_ELEMENT)) {
+		    if(Configuration.getMode() <= threshold) {
+			String args[] = new String[1];
+			args[0] = element.getAttribute(PLUGIN_ATTRIBUTE);    
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadPluginOptions", args), event.getStatus()));
+			args = null;
+		    }
+		}
+		// or the classifier
+		else if (name.equals(BADCLASSIFIER_ELEMENT)) {
+		    if(Configuration.getMode() <= threshold) {
+			String args[] = new String[1];
+			args[0] = element.getAttribute(CLASSIFIER_ATTRIBUTE);    
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadClassifierOptions", args), event.getStatus()));
+			args = null;
+		    }
+		}
+		
+		else {
+		    // Unknown command, go indeterminate
+		    DebugStream.println("Unknown name: " + name);
+		    progress_bar.setIndeterminate(true);
+		}
+		name = null;
+		break;
+	    case COMPRESSTEXT:
+		// We are either dealing with a phase, aa fatal error or an end stage
+		name = element.getElementName();
+		if(name.equals(PHASE_ELEMENT)) {
+		    // There are three phases within compressing text (or of four possible ones)
+		    value = element.getAttribute(NAME_ATTRIBUTE);
+		    if(value.equals(COLLECTTEXTSTATS_VALUE)) {
+			// Nothing to do
+		    }
+		    else if(value.equals(CREATINGCOMPRESS_VALUE) || value.equals(SKIPCREATINGCOMP_VALUE)) {
+			// One third complete
+			progress_bar.setIndeterminate(false);
+			setValue((int)(((double)current_stages + 0.3d) * MAX) / expected_stages);
+		    }
+		    else if(value.equals(COMPRESSINGTEXT_VALUE)) {
+			// Two thirds complete
+			progress_bar.setIndeterminate(false);
+		        setValue((int)(((double)current_stages + 0.6d) * MAX) / expected_stages);
+		    }
+		    else {
+			// Unknown command, go indeterminate
+			DebugStream.println("Unknown value: " + value);
+			progress_bar.setIndeterminate(true);
+		    }
+		    value = null;
+		}
+		// Note that a fatal error is always followed by an end-stage, but not an end build. Thus the final state will be build (not base) and calls to exitValue() with indicate failure.
+		else if(name.equals(FATALERROR_ELEMENT)) {
+		    // On simplier modes produce a meaningful message
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.FatalError"), event.getStatus()));
+		}
+		else if(name.equals(STAGE_ELEMENT) && element.isClosed()) {
+		    // Increase progress so that stage is complete.
+		    current_stages++;
+		    progress_bar.setIndeterminate(false);
+		    setValue((current_stages * MAX) / expected_stages);
+		    state = BUILD;
+		}
+		else {
+		    // Unknown command, go indeterminate
+		    DebugStream.println("Unknown name: " + name);
+		    progress_bar.setIndeterminate(true);
+		}
+		name = null;
+		break;
+	    case INDEX:
+		/** @todo - If on lower modes output a meaningful message. */
+		name = element.getElementName();
+		if(name.equals(PHASE_ELEMENT)) {
+		    value = element.getAttribute(NAME_ATTRIBUTE);
+		    if(value.equals(CREATINGINDEXDIC_VALUE)) {
+			// Nothing to do
+		    }
+		    else if(value.equals(INVERTINGTEXT_VALUE)) {
+			// One fifth complete
+			progress_bar.setIndeterminate(false);
+			setValue((int)(((double)current_stages + 0.2d) * MAX) / expected_stages);
+		    }
+		    else if(value.equals(CREATETHEWEIGHTS_VALUE)) {
+			// Two fifths complete
+			progress_bar.setIndeterminate(false);
+			setValue((int)(((double)current_stages + 0.4d) * MAX) / expected_stages);
+		    }
+		    else if(value.equals(CREATESTEMMEDDIC_VALUE)) {
+			// Three fifths complete
+			progress_bar.setIndeterminate(false);
+			setValue((int)(((double)current_stages + 0.6d) * MAX) / expected_stages);
+		    }
+		    else if(value.equals(CREATINGSTEMINDX_VALUE)) {
+			// Four fifths complete
+			progress_bar.setIndeterminate(false);
+			setValue((int)(((double)current_stages + 0.8d) * MAX) / expected_stages);
+		    }
+		    else {
+			// Unknown command, go indeterminate
+			DebugStream.println("Unknown value: " + value);
+			progress_bar.setIndeterminate(true); 
+		    }
+		    value = null;
+		}
+		else if(name.equals(WARNING_ELEMENT)) {
+		    /** @todo - If on lower modes output a meaningful message. */
+		}
+		// Note that a fatal error is always followed by an end-stage, but not an end build. Thus the final state will be build (not base) and calls to exitValue() with indicate failure.
+		else if(name.equals(FATALERROR_ELEMENT)) {
+		    // On lower modes output a meaningful message.
+		    
+		}
+		else if(name.equals(STAGE_ELEMENT) && element.isClosed()) {
+		    // Increase progress so that stage is complete.
+		    current_stages++;
+		    progress_bar.setIndeterminate(false);
+		    setValue((current_stages * MAX) / expected_stages);
+		    state = BUILD;
+		}
+		else {
+		    // Unknown command, go indeterminate
+		    DebugStream.println("Unknown name: " + name);
+		    progress_bar.setIndeterminate(true); 
+		}
+		name = null;
+		break;
+	    case PHIND:
+		name = element.getElementName();
+		if(name.equals(PHASE_ELEMENT)) {
+		    value = element.getAttribute(NAME_ATTRIBUTE);
+		    if(value.equals(EXTRACTINGVOCAB_VALUE)) {
+			// Nothing to do
+		    }
+		    else if(value.equals(EXTRACTINGPHRASE_VALUE)) {
+			// One sixth complete
+			progress_bar.setIndeterminate(false);
+			setValue((int)(((double)current_stages + 0.16d) * MAX) / expected_stages);
+		    }
+		    else if(value.equals(SORTANDRENUMBER_VALUE)) {
+			// Two sixths complete
+			progress_bar.setIndeterminate(false);
+		        setValue((int)(((double)current_stages + 0.33d) * MAX) / expected_stages);
+		    }
+		    else if(value.equals(PHRASEDATABASES_VALUE)) {
+			// Three sixths complete
+			progress_bar.setIndeterminate(false);
+		        setValue((int)(((double)current_stages + 0.5d) * MAX) / expected_stages);
+		    }
+		    else if(value.equals(WORDLEVELINDEXES_VALUE)) {
+			// Four sixths complete
+			progress_bar.setIndeterminate(false);
+			setValue((int)(((double)current_stages + 0.66d) * MAX) / expected_stages);
+		    }
+		    else if(value.equals(DOCINFODATABASES_VALUE)) {
+			// Five sixths complete
+			progress_bar.setIndeterminate(false);
+			setValue((int)(((double)current_stages + 0.83d) * MAX) / expected_stages);
+		    }
+		    else {
+			// Unknown command, go indeterminate
+			DebugStream.println("Unknown value: " + value);
+			progress_bar.setIndeterminate(true); 
+		    }
+		    value = null;
+		}
+		else if(name.equals(STAGE_ELEMENT) && element.isClosed()) {
+		    // Increase progress so that stage is complete.
+		    current_stages++;
+		    progress_bar.setIndeterminate(false);
+		    setValue((current_stages * MAX) / expected_stages);
+		    state = BUILD;
+		}
+		else {
+		    // Unknown command, go indeterminate
+		    DebugStream.println("Unknown name: " + name);
+		    progress_bar.setIndeterminate(true); 
+		}
+		name = null;
+		break;
+	    case CREATEINFODATA:
+	    case CREATINGAUXILARY:
+		name = element.getElementName();
+		if(name.equals(STAGE_ELEMENT) && element.isClosed()) {
+		    // Another stage complete
+		    current_stages++;
+		    progress_bar.setIndeterminate(false);
+		    setValue((current_stages * MAX) / expected_stages);
+		    state = BUILD;
+		}
+		else {
+		    // Unknown command, go indeterminate
+		    DebugStream.println("Unknown name: " + name);
+		    progress_bar.setIndeterminate(true);
+		}
+		name = null;
+		break;
+	    default:
+		// Unknown command, go indeterminate
+		DebugStream.println("Unknown name: " + name);
+		progress_bar.setIndeterminate(true);
+	    }
+	}
+	// If we are dealing with lower detail modes then veto any other messages. We do this here as the progress monitors are garuenteed to recieve this message before anything else
+	else if(Configuration.getMode() <= threshold) {
+	    event.veto();
+	}
+    }
+
+    public void reset() {
+	current_stages = 0;
+	expected_stages = 0;
+	progress_bar.setIndeterminate(false);
+	progress_bar.setString(null);
+	setValue(MIN);
+	state = BASE;
+	progress_bar.updateUI();
+    }
+
+    public void saving() {
+    }
+
+    public void setSharedProgressBar(GProgressBar shared_progress_bar) {
+	this.shared_progress_bar = shared_progress_bar;
+    }
+
+    /** Since the creator of this process monitor is actually in the GUI, this class provides the simpliest way to send a cancel process message between the two.
+     * @param state The desired state of the stop flag as a <strong>boolean</strong>.
+     */
+    public synchronized void setStop(boolean state) {
+	this.stop = state;
+	progress_bar.setIndeterminate(false);
+    }
+    /** This method resets this monitor to the start, reseting the process parsing and progress bar.
+     * TODO Everthing.
+     */
+    public void start() {
+	stop = false;
+	setValue(MIN);
+	expected_stages = 3; // Always compress text, create info database and auxiliary creation
+	// Retrieve cdm.
+	CollectionDesignManager cdm = Gatherer.c_man.getCollection().cdm;
+	// If we are using MGPP the number of indexes is always 1
+	if(cdm.index_manager.isMGPP()) {
+	    expected_stages = expected_stages + 1;
+	} else if (cdm.index_manager.isLucene()) {
+	    // lucene no of indexes == number of levels
+	    int num_levels = cdm.index_manager.getNumLevels();
+	    if (num_levels == 0) num_levels = 1;
+	    expected_stages = expected_stages + num_levels;
+	}
+	else {
+	    int num_indexes = cdm.index_manager.getNumIndexes();
+	    int num_partitions = cdm.subcollectionindex_manager.getSize();
+	    if(num_partitions > 0) {
+		num_indexes = num_indexes * num_partitions;
+	    }
+	    int num_languages = cdm.language_manager.getSize();
+	    if(num_languages > 0) {
+		num_indexes = num_indexes * num_languages;
+	    }
+	    expected_stages = expected_stages + num_indexes;
+	}
+	// And if we have a phind classifier, we have one further index
+	if(cdm.classifier_manager.isPhindClassifierAssigned()) {
+	    expected_stages = expected_stages + 1;
+	}
+	cdm = null;
+    }
+    /** This method indicates the process is complete. 
+     * TODO Everthing.
+     */
+    public void stop() {
+	progress_bar.setIndeterminate(false);
+	setValue(MAX);
+    }
+
+    private void setValue(int value) {
+	progress_bar.setValue(value);
+	// Incrementing the shared progress bar is a little more problematic as we may very well be trying to set it to MIN but instead set it to halfway if we're not careful.
+	if(value > MIN) {
+	    shared_progress_bar.setValue(MAX + value);
+	}
+    }
+}
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GDefaultProgressMonitor.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GDefaultProgressMonitor.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GDefaultProgressMonitor.java	(revision 31635)
@@ -0,0 +1,90 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.shell;
+
+import org.greenstone.gatherer.gui.GProgressBar;
+import java.util.ArrayList;
+import java.awt.Component;
+
+public class GDefaultProgressMonitor implements GShellProgressMonitor{
+    private GProgressBar progress_bar = null; 
+    private boolean signalledStop = false;    
+
+    public void addProgressBar(GProgressBar progressBar){
+	progress_bar =  progressBar; 
+     }
+
+    public int exitValue(){
+	if (signalledStop){
+	    return 0;
+	}
+	return 1;
+      }
+
+    public GProgressBar getSharedProgress(){
+         return progress_bar;
+     }
+
+    public Component getProgress(){
+        return null;
+    }
+
+    public void messageOnProgressBar(String message){
+
+    }
+
+    public boolean hasSignalledStop(){
+	return signalledStop;
+    }
+   
+    public void increment(){}
+
+    public void process(ArrayList queue){}
+
+    public void reset(){}
+
+    public void saving(){}
+   
+    public void setStop(boolean state){
+        signalledStop = state; 
+    }
+
+    public void start(){}
+
+    public void stop(){}
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GImportProgressMonitor.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GImportProgressMonitor.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GImportProgressMonitor.java	(revision 31635)
@@ -0,0 +1,481 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.shell;
+
+import java.awt.Component;
+import java.util.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.gui.GProgressBar;
+import org.greenstone.gatherer.util.StaticStrings;
+
+/** This implementation of <i>GShellProgressMonitor</i> is designed to parse and translate the progress of a import.pl call.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.1
+ */
+public class GImportProgressMonitor
+    implements GShellProgressMonitor {
+    /** Indicates if the progress bar is currently showing a string. */
+    private boolean showing_string = false;
+    /** Indicates if the GUI has asked the process this object monitors to stop. */
+    private boolean stop = false;
+    /** A count of the extracted files processed so far. */
+    private int extracted_file_count;
+    /** The number of documents processed (or rejected) so far. */
+    private int file_count;
+    /** The next value to be set for the progress bar - I use this rather than a compounding progress measure to try to limit rounding inaccuracies (nothing looks worse than the progress bar having to jump the last 10-15%) */
+    private int next_progress_value;
+    /** The number of files expected to be scanned by this import process. */
+    private int num_expected_docs;
+    /** This holds the number of documents actually processed by the import command, as garnered from the final block of text output. */
+    private int num_docs;
+
+    private int threshold = Configuration.LIBRARIAN_MODE;
+    /** The progress bar this monitor updates. */
+    private GProgressBar progress_bar;
+
+    /** The progress bar that is shared with build monitor. */
+    private GProgressBar shared_progress_bar;
+
+    /** */
+    static final private String BLOCKED_ATTRIBUTE = "blocked";
+    static final private String CONSIDERED_ATTRIBUTE = "considered";
+    static final private String FAILED_ATTRIBUTE = "failed";
+    static final private String IGNORED_ATTRIBUTE = "ignored";
+    static final private String IMPORT_ELEMENT = "Import";
+    static final private String PROCESSED_ATTRIBUTE = "processed";  
+
+    static final private String ARGUMENT_ATTRIBUTE = "a";
+    /** */
+    static final private String NAME_ATTRIBUTE = "n";
+    /** */
+    static final private String PLUGIN_ATTRIBUTE = "p";
+    static final private String REASON_ATTRIBUTE = "r";
+    /** The fixed portion of the progress bar used for the calculating of file size and other pre-import functions. */
+    static final private int CALCULATION = 50000;
+    /** The fixed portion of the progress bar used for extracted metadata. */
+    static final private int EXTRACTED = 200000;
+    /** The fixed portion of the progress bar used for processed documents. */
+    static final private int PROCESSED = 750000;
+    /** The maximum value for this progress bar. */
+    static final private int MAXIMUM = CALCULATION + EXTRACTED + PROCESSED;
+    /** The minimum value for this progress bar. */
+    static final private int MINIMUM = 0;
+    /** The element name of a file detected message. */
+    static final private String FILE_ELEMENT = "File";
+    /** The element name of a file processing message. */
+    static final private String PROCESSING_ELEMENT = "Processing";
+    /** The element name of a processing error message. */
+    static final private String PROCESSINGERROR_ELEMENT = "ProcessingError";
+    /** The element name of an import complete message. */
+    static final private String IMPORTCOMPLETE_ELEMENT = "ImportComplete";
+    /** The element name of a file not processed. */
+    static final private String NONPROCESSEDFILE_ELEMENT = "NonProcessedFile";
+    /** The element name of a file not recognised. */
+    static final private String NONRECOGNISEDFILE_ELEMENT = "NonRecognisedFile";
+    /** The element name of a Warning message */
+    static final private String WARNING_ELEMENT = "Warning";
+    /** The element name of a BadArgument message */
+    static final private String BADARGUMENT_ELEMENT = "BadArgument";
+    static final private String BADARGUMENTVALUE_ELEMENT = "BadArgumentValue";
+    static final private String BADPLUGIN_ELEMENT = "BadPlugin";
+
+    public GImportProgressMonitor() {
+	progress_bar = new GProgressBar();
+	progress_bar.setIndeterminate(false);
+	progress_bar.setMaximum(MAXIMUM);
+	progress_bar.setMinimum(MINIMUM);
+	progress_bar.setString(null);
+	progress_bar.setStringPainted(true);
+	next_progress_value = CALCULATION;
+	shared_progress_bar = new GProgressBar();
+	shared_progress_bar.setIndeterminate(false);
+	shared_progress_bar.setMaximum(MAXIMUM * 2);
+	shared_progress_bar.setMinimum(MINIMUM);
+	shared_progress_bar.setString(null); // Default string
+	shared_progress_bar.setStringPainted(true);
+	setValue(MINIMUM);
+    }
+
+    /** Method to register a new progress bar with this monitor.
+     * @param progress_bar The new <strong>GProgressBar</strong>.
+     */
+    public void addProgressBar(GProgressBar progress_bar) {
+	this.progress_bar = progress_bar;
+	progress_bar.setMaximum(MAXIMUM);
+	progress_bar.setMinimum(MINIMUM);
+	setValue(MINIMUM);
+	next_progress_value = CALCULATION;
+    }
+
+    /** Determine the script exit value according to the progress monitor. This gets around a problem where several script failures actually result with a successful exit value.
+     * @return A <i>int</i> with a value of zero if and only if the script was successful.
+     */
+    public int exitValue() {
+	if(num_docs > 0) {
+	    return 0;
+	}
+	return 1;
+    }
+
+    /** Retrieve the number of documents recorded by the import monitor during the execution of the import scripts.
+     * @return An <i>int</i> giving the document number.
+     */
+    public int getNumberOfDocuments() {
+	return num_docs;
+    }
+
+    /** Method to retrieve whatever control is being used as the progress indicator. Usually a <strong>GProgressBar</strong> but there may be others implemented later.
+     * @return A <strong>Component</strong> on which the progress of the process is being displayed.
+     */
+    public Component getProgress() {
+	return progress_bar;
+    }
+
+    public GProgressBar getSharedProgress() {
+	return shared_progress_bar;
+    }
+
+    public void messageOnProgressBar(String message)
+    {
+	if (message!=null && !message.equals("")) {
+	    progress_bar.setString(message);
+	    shared_progress_bar.setString(message);
+	    showing_string = true;
+	}
+	else {
+	    progress_bar.setString(null);
+	    shared_progress_bar.setString(null);
+	    showing_string = false;
+	}
+    }
+
+    /** Method to determine the state of the stop flag, which may be set by the visual component that created this monitor.
+     * @return A <strong>boolean</strong> indicating if the process has been asked to stop.
+     */
+    public synchronized boolean hasSignalledStop() {
+	return stop;
+    }
+
+    /** Inform the progress bar that it should programatically increment progress by one step. This is only called during the metadata archive extraction so each step should be (1000000 / 5) / num_docs. */
+    public void increment() {
+	extracted_file_count++;
+	// The current progress is calculated to be:
+	// The fixed calculation value plus the fixed processed value plus some portion of the fixed extracted value. This portion is the extracted_file_count over the total number of documents available. Note that this breaks badly for bibliographical files (for now).
+	setValue(CALCULATION + PROCESSED + ((EXTRACTED * extracted_file_count) / num_docs));
+    }
+
+    /** This method is used to 'feed in' a line of text captured from the process.
+     * @param queue a queue which at the moment should contain a single GShellEvent
+     */
+    public void process(ArrayList queue) {
+	// Retrieve the first event.
+	GShellEvent event = (GShellEvent) queue.get(0);
+	// Remove 'import.pl> ' bit
+	String line = event.getMessage();
+	line = line.substring(line.indexOf(StaticStrings.GREATER_THAN_CHARACTER) + 1);
+	line = line.trim();
+	// System.err.println("**** line = " + line);
+
+	///ystem.err.println("Process: " + line);
+	if(line.startsWith(StaticStrings.LESS_THAN_CHARACTER) && line.endsWith(StaticStrings.GREATER_THAN_CHARACTER)) {
+	    // No original event is passed on, even in the lower modes
+	    event.veto();
+	    // Create a new element from it
+	    GShellElement element = new GShellElement(line);
+	    String name = element.getElementName();
+	    ///ystem.err.println("Gatherer tag: " + name);
+	    // We may be reading a file. Remember we have to offset process as we recieve this message 'before' a document is processed. Hence the use of 'next_progress_value'
+	    if(name.equals(IMPORT_ELEMENT)) {
+		///ystem.err.println("#Import");
+		// We're into parsing output, so we don't need the 'calculating file size' etc string.
+		if(showing_string) {
+		    progress_bar.setString(null);
+		    showing_string = false;
+		}
+		if(Configuration.getMode() <= threshold) {
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.ImportBegun1"), event.getStatus()));
+		}
+	    }
+	    else if(name.equals(FILE_ELEMENT)) {
+		///ystem.err.println("#File");
+		file_count++;
+		// Set the next progress
+		if(next_progress_value > 0) {
+		    setValue(next_progress_value);
+		}
+		// Now we calculate the next progress value
+		next_progress_value = CALCULATION + ((PROCESSED * file_count) / num_expected_docs);
+		event.veto(); // Unconditional veto
+	    }
+	    // Or we're being told what plugin is actually processing the file
+	    else if(name.equals(PROCESSING_ELEMENT)) {
+		///ystem.err.println("#FileProcessing");
+		// If we are at lower mode settings fire a new 'dumbed down' event
+		if(Configuration.getMode() <= threshold) {
+		    String args[] = new String[2];
+		    args[0] = element.getAttribute(NAME_ATTRIBUTE);
+		    args[1] = element.getAttribute(PLUGIN_ATTRIBUTE);
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.FileProcessing", args), event.getStatus()));
+		    args = null;
+		}
+	    }
+	    // processing error
+	    else if (name.equals(PROCESSINGERROR_ELEMENT)) {
+		if(Configuration.getMode() <= threshold) {
+		    String args[] = new String[1];
+		    args[0] = element.getAttribute(NAME_ATTRIBUTE);
+		    String reason = element.getAttribute(REASON_ATTRIBUTE);
+		    if (reason == null || reason.equals("")) {
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.FileProcessingError", args), event.getStatus()));
+		    }
+		    else {
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.FileProcessingError", args) + " (" + reason + ")", event.getStatus()));
+		    }
+		    args = null;
+		}
+	    }
+	    // unrecognised file
+	    else if (name.equals(NONRECOGNISEDFILE_ELEMENT)) {
+		if(Configuration.getMode() <= threshold) {
+		    String args[] = new String[1];
+		    args[0] =element.getAttribute(NAME_ATTRIBUTE);
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.FileNotRecognised", args), event.getStatus()));
+		    args = null;
+		}
+	    }
+	    // unprocessed file
+	    else if (name.equals(NONPROCESSEDFILE_ELEMENT)) {
+		if(Configuration.getMode() <= threshold) {
+		    String args[] = new String[1];
+		    args[0] =element.getAttribute(NAME_ATTRIBUTE);		    
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.FileNotProcessed", args), event.getStatus()));
+		    args = null;
+		}
+	    }
+	    // We had a bad argument to a plugin
+	    else if (name.equals(BADARGUMENT_ELEMENT)) {
+		if(Configuration.getMode() <= threshold) {
+		    String args[] = new String[1];
+		    args[0] = element.getAttribute(ARGUMENT_ATTRIBUTE);    
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadArgument", args), event.getStatus()));
+		    args = null;
+		}
+	    }
+	    // We had a bad argument value to a plugin
+	    else if (name.equals(BADARGUMENTVALUE_ELEMENT)) {
+		if(Configuration.getMode() <= threshold) {
+		    String args[] = new String[1];
+		    args[0] = element.getAttribute(ARGUMENT_ATTRIBUTE);    
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadArgumentValue", args), event.getStatus()));
+		    args = null;
+		}
+	    }
+	    // And this one tellsa us the plugin
+	    else if (name.equals(BADPLUGIN_ELEMENT)) {
+		if(Configuration.getMode() <= threshold) {
+		    String args[] = new String[1];
+		    args[0] = element.getAttribute(PLUGIN_ATTRIBUTE);    
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadPluginOptions", args), event.getStatus()));
+		    args = null;
+		}
+	    }
+	    else if (name.equals(WARNING_ELEMENT)) {
+		if(Configuration.getMode() <= threshold) {
+		    String args[] = new String[1];
+		    args[0] = element.getAttribute(PLUGIN_ATTRIBUTE);
+		    String reason = element.getAttribute(REASON_ATTRIBUTE);
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.Warning", args) + " " + reason, event.getStatus()));
+		    args = null;
+		}
+	    }
+	    
+	    
+	    // Or the import complete element
+	    else if(name.equals(IMPORTCOMPLETE_ELEMENT)) {
+		// Set the next progress
+		setValue(next_progress_value);
+		// If we are at lower mode settings fire a new 'dumbed down' event
+		if(Configuration.getMode() <= threshold) {
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.ImportComplete1"), event.getStatus()));
+		    String considered_str = element.getAttribute(CONSIDERED_ATTRIBUTE);
+
+		    if (considered_str.equals(StaticStrings.ONE_CHARACTER)) {
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.ImportComplete2.1"), event.getStatus()));	
+		    } else {
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.ImportComplete2", considered_str), event.getStatus()));
+		    }
+		    considered_str = null;
+		    // The number of documents processed
+		    String processed_str = element.getAttribute(PROCESSED_ATTRIBUTE);
+		    if(!processed_str.equals(StaticStrings.ZERO_CHARACTER)) {
+			if(processed_str.equals(StaticStrings.ONE_CHARACTER)) {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), "    "+Dictionary.get("GShell.Import.ImportComplete.Processed.1"), event.getStatus()));
+			}
+			else {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), "    "+Dictionary.get("GShell.Import.ImportComplete.Processed", processed_str), event.getStatus()));
+			}
+		    }
+		    // Try to set num docs
+		    try {
+			num_docs = Integer.parseInt(processed_str);
+		    }
+		    catch(Exception exception) {
+			num_docs = 0;
+		    }  
+		    processed_str = null;
+
+		    // The number of documents blocked
+		    String blocked_str = element.getAttribute(BLOCKED_ATTRIBUTE);
+		    if(!blocked_str.equals(StaticStrings.ZERO_CHARACTER)) {
+			if (blocked_str.equals(StaticStrings.ONE_CHARACTER)) {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), "    "+Dictionary.get("GShell.Import.ImportComplete.Blocked.1"), event.getStatus()));
+			} else {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), "    "+Dictionary.get("GShell.Import.ImportComplete.Blocked", blocked_str), event.getStatus()));
+			}
+		    }
+		    blocked_str = null;
+		    
+		    // The number of documents ignored
+		    String ignored_str = element.getAttribute(IGNORED_ATTRIBUTE);
+		    if(!ignored_str.equals(StaticStrings.ZERO_CHARACTER)) {
+			if(ignored_str.equals(StaticStrings.ONE_CHARACTER)) {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), "    "+Dictionary.get("GShell.Import.ImportComplete.Ignored.1"), event.getStatus()));
+			} else {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), "    "+Dictionary.get("GShell.Import.ImportComplete.Ignored", ignored_str), event.getStatus()));
+			}
+		    }
+		    ignored_str = null;
+		    
+		    // The number of documents failed
+		    String failed_str = element.getAttribute(FAILED_ATTRIBUTE);
+		    if(!failed_str.equals(StaticStrings.ZERO_CHARACTER)) {
+			if(failed_str.equals(StaticStrings.ONE_CHARACTER)) {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), "    "+Dictionary.get("GShell.Import.ImportComplete.Failed.1"), event.getStatus()));
+			} else {
+			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), "    "+Dictionary.get("GShell.Import.ImportComplete.Failed", failed_str), event.getStatus()));
+			}
+		    }
+		    failed_str = null;
+		    // Message is finally ready
+		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Import.ImportComplete3"), event.getStatus()));
+		} // if mode below threshhold
+		else {
+		    // Try to set num docs
+		    String processed_str = element.getAttribute(PROCESSED_ATTRIBUTE);
+		    try {
+			num_docs = Integer.parseInt(processed_str);
+		    }
+		    catch(Exception exception) {
+			num_docs = 0;
+		    }
+		}
+	    }
+	    else {
+		// Veto it
+		event.veto();
+	    }
+	} // GLI output
+	else if(Configuration.getMode() <= threshold) {
+	    event.veto();
+	}
+    }
+
+    public void reset() {
+	setValue(MINIMUM);
+	progress_bar.setString(null);
+	progress_bar.updateUI();
+    }
+
+    public void saving() {
+	progress_bar.setString(Dictionary.get("SaveProgressDialog.Title"));
+	showing_string = true;
+    }
+
+    /** Since the creator of this process monitor is actually in the GUI, this class provides the simpliest way to send a cancel process message between the two.
+     * @param state The desired state of the stop flag as a <strong>boolean</strong>.
+     */
+    public synchronized void setStop(boolean state) {
+	this.stop = state;
+    }
+
+    /** This method resets this monitor to the start, reseting the process parsing and progress bar.
+     */
+    public void start() {
+	stop = false;
+	setValue(MINIMUM);
+	progress_bar.setString(Dictionary.get("FileActions.Calculating_Size"));
+	showing_string = true;
+	extracted_file_count = 0;
+	file_count = 0;
+	next_progress_value = -1;
+	num_docs = -1;
+	// !! HACK: This is multiplied by 2 because the metadata_read now outputs a <File> tag also
+	// Ideally the metadata read pass would be completely separate and the GLI could work out the number
+	//   of documents accurately and provide a much better progress bar
+	num_expected_docs = Gatherer.c_man.getCollection().getCount() * 2;
+    }
+
+    /** This method indicates the process is complete. 
+     */
+    public void stop() {
+	setValue(MAXIMUM);
+    }
+
+    int previous_value = -1;
+
+    private void setValue(int value)
+    {
+	// Don't let the value go higher than the maximum
+	if (value > MAXIMUM) {
+	    DebugStream.println("Error: Tried to set progress bar higher than maximum!\n");
+	    return;
+	}
+	// Don't let the value go backwards
+	if (value != 0 && value < previous_value) {
+	    DebugStream.println("Error: Tried to set progress bar smaller than previous!\n");
+	    return;
+	}
+
+	progress_bar.setValue(value);
+	shared_progress_bar.setValue(value);
+	previous_value = value;
+    }
+}
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GScheduleProgressMonitor.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GScheduleProgressMonitor.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GScheduleProgressMonitor.java	(revision 31635)
@@ -0,0 +1,262 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.shell;
+
+import java.awt.Component;
+import java.util.ArrayList;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.gui.GProgressBar;
+import org.greenstone.gatherer.util.StaticStrings;
+/** This implementation of <i>GShellProgressMonitor</i> is designed to parse and translate the progress of a buildcol.pl call.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+/* Wendy's feeble attempt at writing a schedule progress monitor. While it's not really needed, I'm writing it so that the schedule.pl prompts will go away. ;)  This also may need other "ELEMENTS" added to it. 
+ */ 
+
+public class GScheduleProgressMonitor
+    implements GShellProgressMonitor {
+    /** The minimum value = 0 */
+    static final private int MIN = 0;
+    /** In order to get a smoothish progress bar, set the maximum to a large number = 1000000 */
+    static final private int MAX = 1000000;
+    /** The various states the Schedule progress state machine can be in, starting at the base state. There are only two stages - perl script generation and cron building - so i'm not sure we need too  many steps. */
+    static final int BASE                          = -1;
+    static final int SCHEDULE                      =  0;
+    static final int DONE                          =  1; 
+    /** Now the language independant sentinel strings. */
+    static final String SCHEDULE_ELEMENT = "Schedule"; 
+    static final String SCHEDULE_DELETE = "Delete"; 
+    static final String SCHEDULE_DONE="Done"; 
+
+    static final private String ARGUMENT_ATTRIBUTE = "a";
+    static final private String CLASSIFIER_ATTRIBUTE = "c";
+    static final private String PLUGIN_ATTRIBUTE = "p";
+
+    /** Indicates if the GUI has asked the process this object monitors to stop. */
+    private boolean stop = false;
+    /** The current number of stages we have completed. */
+    private int current_stages = 0;
+    /** The number of stages we are expecting to complete. */
+    private int expected_stages = 0;
+
+    private int threshold = Configuration.LIBRARIAN_MODE;
+    /** The current state we are in. The state will change depending on the next flag recieved. */
+    private int state = BASE;
+    /** The progress bar this monitor updates. */
+    //private GProgressBar progress_bar;
+    /** A progress bar that is shared between this this listener and the Import and build monitor. not sure we need this. */
+    //private GProgressBar shared_progress_bar;
+
+    /** Construct a new GScheduleProgressMonitor. */
+    public GScheduleProgressMonitor() {
+	//this.shared_progress_bar = shared_progress_bar;
+	//progress_bar = new GProgressBar();
+	//progress_bar.setMaximum(MAX);
+	//progress_bar.setMinimum(MIN);
+	//progress_bar.setString(null);
+	//progress_bar.setStringPainted(true);
+	//setValue(MIN);
+    }
+
+    /** Method to register a new progress bar with this monitor.
+     * @param progress_bar The new <strong>GProgressBar</strong>.
+     */	 
+    /*note - we don't really need this. Wendy */ 
+    public void addProgressBar(GProgressBar progress_bar) {
+	//this.progress_bar = progress_bar;
+	//progress_bar.setMaximum(MAX);
+	//progress_bar.setMinimum(MIN);
+	//progress_bar.setStringPainted(false);
+	//progress_bar.setString(null);
+	//setValue(MIN);
+    }
+
+    /** Determine the script exit value according to the progress monitor. This gets around a problem where several script failures actually result with a successful exit value.
+     * @return A <i>int</i> with a value of zero if and only if the script was successful.
+     */
+    public int exitValue() {
+	if(state == BASE) {
+	    return 0;
+	}
+	return 1;
+    }
+    /** Method to retrieve whatever control is being used as the progress indicator. Usually a <strong>GProgressBar</strong> but there may be others implemented later.
+     * @return A <strong>Component</strong> on which the progress of the process is being displayed.
+     */
+    public Component getProgress() {
+	//return progress_bar;
+	return null;
+    }
+
+    public GProgressBar getSharedProgress() {
+    //return shared_progress_bar;
+    //try returning null;
+	return null; 
+    }
+
+    public void messageOnProgressBar(String message)
+    {
+	//if (message!=null && !message.equals("")) {
+	    //progress_bar.setString(message);
+	    //shared_progress_bar.setString(message);
+	//}
+        //else {
+	    //progress_bar.setString(null);
+	    //shared_progress_bar.setString(null);
+        //}
+    }
+
+    /** Method to determine the state of the stop flag, which may be set by the visual component that created this monitor.
+     * @return A <strong>boolean</strong> indicating if the process has been asked to stop.
+     */
+    public synchronized boolean hasSignalledStop() {
+	return stop;
+    }
+    /** Inform the progress bar that it should programatically increment progress by one step. */
+    public void increment() {
+	// I'm not sure we really need this for scheduling, so i'm going to ignore it for now.
+    }
+
+    /** This method is used to 'feed in' a line of text captured from the process.
+     * @param queue a queue which at the moment should contain a single GShellEvent
+     */
+    public void process(ArrayList queue) {
+	GShellEvent event = (GShellEvent) queue.get(0);
+	// Remove 'schedule.pl> ' bit
+	String line = event.getMessage();
+	line = line.substring(line.indexOf(StaticStrings.GREATER_THAN_CHARACTER) + 1);
+	line = line.trim();
+	// We are only really interested in processing pseudo-XML tags 
+	if(line.startsWith(StaticStrings.LESS_THAN_CHARACTER) && line.endsWith(StaticStrings.GREATER_THAN_CHARACTER)) {
+	    // All events are vetoed
+	    event.veto();
+	    
+	    // Create a new element from it
+	    GShellElement element = new GShellElement(line);
+	    // Now change states within the state machine as appropriate. A state change may subsequently incur a progress update.
+	    String name = null;
+	    String value = null;
+
+		name = element.getElementName(); 
+		if(name.equals(SCHEDULE_ELEMENT)) {
+		    //progress_bar.setIndeterminate(false);
+		    // Produce a lower detail message if required
+		    if(Configuration.getMode() <= threshold && !stop) {
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Schedule.ScheduleBegun1"), event.getStatus()));
+		    }
+		}
+		else if(name.equals(SCHEDULE_DELETE)) { 
+		    if(Configuration.getMode() <= threshold) {
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Schedule.ScheduleDelete"), event.getStatus()));
+		    }
+		}
+		else if(name.equals(SCHEDULE_DONE)) { 
+		    if(Configuration.getMode() <= threshold) {
+			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Schedule.ScheduleComplete1"), event.getStatus()));
+		    }
+		} 
+		else {
+		    // Unknown command, go indeterminate
+		    DebugStream.println("Unknown name: " + name);
+		    //progress_bar.setIndeterminate(true);
+		}
+
+
+
+	}
+	// If we are dealing with lower detail modes then veto any other messages. We do this here as the progress monitors are garuenteed to recieve this message before anything else
+	else if(Configuration.getMode() <= threshold) {
+	    event.veto();
+	}
+    }
+
+    public void reset() {
+	current_stages = 0;
+	expected_stages = 0;
+	//progress_bar.setIndeterminate(false);
+	//progress_bar.setString(null);
+	setValue(MIN);
+	state = BASE;
+	//progress_bar.updateUI();
+    }
+
+    public void saving() {
+    }
+
+    public void setSharedProgressBar(GProgressBar shared_progress_bar) {
+    //this.shared_progress_bar = shared_progress_bar;
+    }
+
+    /** Since the creator of this process monitor is actually in the GUI, this class provides the simpliest way to send a cancel process message between the two.
+     * @param state The desired state of the stop flag as a <strong>boolean</strong>.
+     */
+    public synchronized void setStop(boolean state) {
+	this.stop = state;
+	//progress_bar.setIndeterminate(false);
+    }
+    /** This method resets this monitor to the start, reseting the process parsing and progress bar.
+     * TODO Everthing.
+     */
+    public void start() {
+	stop = false;
+	setValue(MIN);
+	expected_stages = 1; // Always compress text, create info database and auxiliary creation
+
+
+    }
+    /** This method indicates the process is complete. 
+     * TODO Everthing.
+     */
+    public void stop() {
+	//progress_bar.setIndeterminate(false);
+	setValue(MAX);
+    }
+
+    private void setValue(int value) {
+	//progress_bar.setValue(value);
+	// Incrementing the shared progress bar is a little more problematic as we may very well be trying to set it to MIN but instead set it to halfway if we're not careful.
+	//if(value > MIN) {
+	//    shared_progress_bar.setValue(MAX + value);
+	//}
+    }
+}
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShell.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShell.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShell.java	(revision 31635)
@@ -0,0 +1,641 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.shell;
+
+import java.io.*;
+import java.net.*;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.regex.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.cdm.CollectionConfiguration;
+import org.greenstone.gatherer.cdm.CollectionDesignManager;
+import org.greenstone.gatherer.cdm.CollectionMetaManager;
+import org.greenstone.gatherer.cdm.CollectionMeta;
+import org.greenstone.gatherer.collection.CollectionManager;
+import org.greenstone.gatherer.metadata.DocXMLFileManager;
+import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
+import org.greenstone.gatherer.util.StaticStrings; 
+import org.greenstone.gatherer.util.Utility;
+
+
+/** The <strong>GShell</strong> is reponsible for running a separately threaded process in the command shell. This is necessary for executing the Perl Scripts and also for other system related funcitonality.
+ */
+public class GShell
+    extends Thread {
+    /** A flag used to determine if this process has been asked to cancel. */
+    private boolean cancel = false;
+    private BufferedOutputStream buffered_output_stream = null;
+    /** The list of listeners associated with this class. */
+    private EventListenerList listeners = null;
+    /** The current status of this shell process. */
+    private int status = -1;
+    /** The type of message being sent. */
+    private int msg_type = -1;
+    /** The type of shell process. */
+    private int type = -1;
+    /** The caller of this process, and thus the class most interested in messages. */
+    private GShellListener caller = null;
+    /** The progress monitor associated with this process. */
+    private GShellProgressMonitor progress = null;
+    /** Arguments to be given to the process (including the executable you are calling. */
+    private String args[] = null;
+    /** The command_output returned from executing a process */
+    private String commandOutput = null;
+
+    /** Elements in process type enumeration. */
+    static final public int BUILD = 0;
+    static final public int IMPORT = 1;
+    static final public int NEW = 2;
+    static final public int EXPORTAS = 3;
+    static final public int CDIMAGE = 4;
+    static final public int CONVERT = 5;
+    static final public int EXPLODE = 6;
+    static final public int SRCREPLACE = 7; // for replacing source docs with their html
+    static final public int SCHEDULE = 8;
+    static final public int DELETE = 9;
+
+    /** Elements in status type enumeration. */
+    static final public int ERROR = 0;
+    static final public int OK = 1;
+    static final public int CANCELLED = 2;
+
+    /** Elements in process type name enumeration. */
+    static public String GSHELL_BUILD = "gshell_build";
+    static public String GSHELL_IMPORT = "gshell_import";
+    static public String GSHELL_NEW = "gshell_new";
+    static public String GSHELL_EXPORTAS = "gshell_exportas";
+    static public String GSHELL_CDIMAGE = "gshell_cdimage";
+    static public String GSHELL_CONVERT = "gshell_convert";
+    static public String GSHELL_EXPLODE = "gshell_explode";
+    static public String GSHELL_SRCREPLACE = "gshell_srcreplace"; // for replacing source docs with their html versions
+    static public String GSHELL_SCHEDULE = "gshell_schedule"; 
+    static public String GSHELL_FEDORA_COLDELETE = "gshell_fedora_col_delete";
+
+    /** Determine if the given process is still executing. It does this by attempting to throw an exception - not the most efficient way, but the only one as far as I know
+     * @param process the Process to test
+     * @return true if it is still executing, false otherwise
+     */
+    static public boolean processRunning(Process process) {
+	boolean process_running = false;
+
+	try {
+	    process.exitValue(); // This will throw an exception if the process hasn't ended yet.
+	}
+	catch(IllegalThreadStateException itse) {
+	    process_running = true;
+	}
+	catch(Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	return process_running;
+    }
+
+    /** Constructor gatherer all the data required to create a new process, and emit meaningfull messages.
+     * @param args A <strong>String[]</strong> containing the arguments to the process thread, including the name of the executable.
+     * @param type An <strong>int</strong> that indicates what group of processes this process belongs to, as some are treated slightly differently (i.e an IMPORT type process is always followed by a BUILD one).
+     * @param msg_type As process threads may be background (like a makecol.pl call) or important processes in their own right (such as an IMPORT-BUILD) we must be able to set what level messages posted by this class will have by usings this <strong>int</strong>.
+     * @param caller The default <i>GShellListener</i> that is interested in the progress of this process.
+     * @param progress The <i>GShellProgressMonitor</i> associated with this process.
+     * @param name A <strong>String</strong> identifier given to the process, for convience and debug reasons.
+     */
+    public GShell(String args[], int type, int msg_type, GShellListener caller, GShellProgressMonitor progress, String name) {
+	super(name);
+	this.args = args;
+	this.msg_type = msg_type;
+	this.type = type;
+	this.caller = caller;
+	this.progress = progress;
+	this.status = 0;
+	// Lower this jobs priority
+	this.setPriority(Thread.MIN_PRIORITY);
+	listeners = new EventListenerList();
+	listeners.add(GShellListener.class, caller);
+    }
+    /** This method adds another shell listener to this process.
+     * @param listener The new <i>GShellListener</i>.
+     */
+    public void addGShellListener(GShellListener listener) {
+	listeners.add(GShellListener.class, listener);
+    }
+    /** This method removes a certain shell listener from this process.
+     * @param listener The <i>GShellListener</i> to be removed.
+     */
+    /* private void removeGShellListener(GShellListener listener) {
+       listeners.remove(GShellListener.class, listener);
+       } */
+
+    protected StringBuffer get_stream_char(InputStreamReader isr, StringBuffer line_buffer,
+				   BufferedOutputStream bos) throws IOException 
+    {
+	int c = isr.read();
+	///atherer.println("isr: '" + (char) c + "'");
+	if(c == '\n' || c == '\r') {
+	    if(line_buffer.length() > 0) {
+		String line = line_buffer.toString();
+		// DebugStream.println("* " + line + " *");		
+		fireMessage(type, typeAsString(type) + "> " + line, status, bos);
+		line_buffer = new StringBuffer();
+	    }
+	}
+	else {
+	    line_buffer.append((char)c);
+	}
+
+	return line_buffer;
+    }
+
+    public String getCommandOutput() { return commandOutput; }
+    public void resetCommandOutput() { commandOutput = null; }
+
+    private void runRemote(String[] args, BufferedOutputStream bos)
+    {
+	// Make sure the process hasn't been cancelled
+	if (hasSignalledStop()) {
+	    return;
+	}
+
+	try {
+	    int directory_name_end = args[0].lastIndexOf(File.separator);
+	    String script_name = ((directory_name_end != -1) ? args[0].substring(directory_name_end + 1) : args[0]);
+	    System.err.println("Script name: " + script_name);
+	    String collection_name = args[args.length - 1];
+	    System.err.println("Collection name: " + collection_name);
+
+
+	    String script_args = "";
+	    for (int i = 1; i < (args.length - 1); i++) {
+		// Skip arguments that won't make sense on the server
+		// and -site is added in RemoteGreenstonServer.java
+		if (args[i].equals("-collectdir") || args[i].equals("-importdir") || args[i].equals("-builddir") || args[i].equals("-site")) {
+		    i++;
+		    continue;
+		}
+
+		// Script arguments get changed to CGI arguments
+		if (args[i].startsWith("-")) {
+		    script_args += "&" + args[i].substring(1) + "=";
+		    if ((i + 1) < (args.length - 1) && !args[i + 1].startsWith("-")) {
+			script_args += URLEncoder.encode(args[i + 1], "UTF-8");
+			i++;
+		    }
+		}
+	    }
+
+	    System.err.println("Script args: " + script_args);
+	    buffered_output_stream = bos;
+	    String command_output = Gatherer.remoteGreenstoneServer.runScript(collection_name, script_name, script_args, this);
+	    this.commandOutput = command_output;
+	    status = (command_output.equals("") ? CANCELLED : OK);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	    status = ERROR;
+	}
+    }
+
+
+    private void runLocal(String[] args, BufferedOutputStream bos) 
+    {
+	try {
+	    String command = "";
+	    for(int i = 0; i < args.length; i++) {
+		command = command + args[i] + " ";
+	    }
+	    
+	    ///ystem.err.println("Command: " + command);
+	    fireMessage(type, Dictionary.get("GShell.Command") + ": " + command, status, null);
+	    
+	    Runtime rt = Runtime.getRuntime();
+	    Process prcs = null;
+	  
+	    prcs = rt.exec(args);
+	    // If we used single argument exec, spaces in filenames or paths cause problems
+	    // I.e. better to avoid: prcs = rt.exec(command);
+	    
+	    InputStreamReader eisr = new InputStreamReader( prcs.getErrorStream(), "UTF-8" );
+	    InputStreamReader stdisr = new InputStreamReader( prcs.getInputStream(), "UTF-8"  );	    
+	
+	    StringBuffer eline_buffer = new StringBuffer();
+	    StringBuffer stdline_buffer = new StringBuffer();
+	
+	    while(/*type != GShell.NEW &&*/ processRunning(prcs) && !hasSignalledStop()) {
+		// Hopefully this doesn't block if the process is trying to write to STDOUT.
+		if((eisr!=null) && eisr.ready()) { 
+		    eline_buffer = get_stream_char(eisr,eline_buffer,bos);
+		}
+		// Hopefully this won't block if the process is trying to write to STDERR
+		else if(stdisr.ready()) {
+		    stdline_buffer = get_stream_char(stdisr,stdline_buffer,bos);
+		}
+		else {
+		    try {
+			sleep(100);
+		    }
+		    catch(Exception exception) {	     
+		    }
+		}
+	    }
+	
+	    if(!hasSignalledStop()) {
+		// Of course, just because the process is finished doesn't
+		// mean the incoming streams are empty. Unfortunately I've
+		// got no chance of preserving order, so I'll process the
+		// error stream first, then the out stream
+		while(eisr.ready()) {
+		    eline_buffer = get_stream_char(eisr,eline_buffer,bos);
+		}
+	    
+		while(stdisr.ready()) {
+		    stdline_buffer = get_stream_char(stdisr,stdline_buffer,bos);
+		}
+	    
+		// Ensure that any messages still remaining in the string buffers are fired off.
+		if(eline_buffer.length() > 0) {
+		    String eline = eline_buffer.toString();
+		    //DebugStream.println("Last bit of eline: " + eline);
+		    fireMessage(type, typeAsString(type) + "> " + eline, status, bos);
+		    eline = null;
+		}
+	    
+		if(stdline_buffer.length() > 0) {
+		    String stdline = stdline_buffer.toString();
+		    //DebugStream.println("Last bit of stdline: " + stdline);
+		    fireMessage(type, typeAsString(type) + "> " + stdline, status, null);
+		    stdline = null;
+		}
+	    }
+	    else {
+		DebugStream.println("We've been asked to stop.");
+	    }
+	
+	
+	    if(!hasSignalledStop()) {
+		// Now display final message based on exit value
+	    
+		prcs.waitFor(); 
+
+		if(prcs.exitValue() == 0) {
+		    status = OK;
+		    fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Success"), status, null);
+		}
+		else {
+		    status = ERROR;
+		    fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Failure"), status, null);
+		}
+	    
+		eisr.close();
+		stdisr.close();
+	    }
+	    else {
+		// I need to somehow kill the child process. Unfortunately
+		// Thread.stop() and Process.destroy() both fail to do
+		// this. But now, thankx to the magic of Michaels 'close the
+		// stream suggestion', it works fine (no it doesn't!)
+		prcs.getInputStream().close();
+		prcs.getErrorStream().close();
+		prcs.getOutputStream().close();
+		prcs.destroy();
+		status = CANCELLED;
+	    }
+	}
+	// Exception
+	catch (Exception exception) {
+	    DebugStream.println("Exception in GShell.runLocal() - unexpected");
+	    DebugStream.printStackTrace(exception);
+	    status = ERROR;
+	}
+    }
+    
+    
+
+    /** Any threaded class must include this method to allow the thread body to be run. */
+    public void run() {
+	String col_name = args[args.length-1];		
+
+	// Determine if the user has asked for an outfile.
+	String out_name = null;
+	BufferedOutputStream bos = null;
+	if(type == IMPORT || type == BUILD || type == SCHEDULE) {
+	    if(type == IMPORT) {
+		out_name = (String) Gatherer.c_man.getCollection().import_options.getValue("out");
+	    }
+	    else if(type == BUILD) {
+		out_name = (String) Gatherer.c_man.getCollection().build_options.getValue("out");
+	    }
+	    else { // SCHEDULE
+		out_name = (String) Gatherer.c_man.getCollection().schedule_options.getValue("out"); 
+	    }
+	    if(out_name != null && out_name.length() > 0) {
+		try {
+		    bos = new BufferedOutputStream(new FileOutputStream(new File(out_name), true));
+		}
+		catch (Exception error) {
+		    DebugStream.printStackTrace(error);
+		}
+	    }
+	}
+
+	// Issue a processBegun event
+	//ystem.err.println("\nFiring process begun for " + type + "...");
+	fireProcessBegun(type, status);
+	//ystem.err.println("Done process begun.");
+	if (Gatherer.isGsdlRemote) {
+	    runRemote(args,bos);
+	}
+	else {
+	    runLocal(args,bos);
+	}
+	//ystem.err.println("Done runLocal().");
+
+	if(status == OK) {
+	    if (type == NEW) {
+		if (Gatherer.isGsdlRemote) {
+		    Gatherer.remoteGreenstoneServer.downloadCollection(col_name);
+		}
+	    }
+	    else if(type == IMPORT) {
+		
+		// download the archives directory (if gsdl server is remote)			
+		if (Gatherer.isGsdlRemote) {
+
+		    if (progress!=null) {
+			progress.messageOnProgressBar("Downloading archive data from server");
+		    }
+
+		    Gatherer.remoteGreenstoneServer.downloadCollectionArchives(col_name);
+
+		    if (progress!=null) {
+			progress.messageOnProgressBar("");
+		    }      
+		}
+
+		// Refresh the DocXMLFileManager
+		fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Start"), status, null);
+		DocXMLFileManager.clearDocXMLFiles();
+		if (Configuration.fedora_info.isActive()) { // FLI case
+		    File collection_export_directory = new File(CollectionManager.getLoadedCollectionExportDirectoryPath());
+		    DocXMLFileManager.loadDocXMLFiles(collection_export_directory,"docmets.xml");
+		}
+		else {
+		    File collection_archives_directory = new File(CollectionManager.getLoadedCollectionArchivesDirectoryPath());
+		    DocXMLFileManager.loadDocXMLFiles(collection_archives_directory,"doc.xml");
+		}
+
+
+		fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Complete"), status, null);
+	    }
+
+ 	    else if(type == BUILD) {
+ 		// download the building directory (if gsdl server is remote)
+ 		if ((Gatherer.isGsdlRemote) && (!Configuration.fedora_info.isActive())) {
+ 		    if (progress!=null) {
+ 			progress.messageOnProgressBar("Downloading index data from server");
+ 		    }
+		    			
+		    if (!Gatherer.GS3){
+			// Only need to download build.cfg file
+			File build_cfg_file = new File(CollectionManager.getLoadedCollectionBuildingDirectoryPath(), "build.cfg");
+			Gatherer.remoteGreenstoneServer.downloadCollectionFile(col_name, build_cfg_file);
+		    }else{
+			// Only need to download buildConfig.xml file
+			File buildConfig_xml_file = new File(CollectionManager.getLoadedCollectionBuildingDirectoryPath(), "buildConfig.xml");
+			Gatherer.remoteGreenstoneServer.downloadCollectionFile(col_name, buildConfig_xml_file);
+		    }
+
+ 		    if (progress!=null) {
+ 			progress.messageOnProgressBar("");
+ 		    }
+ 		}
+ 	    }
+	    else if(type == CDIMAGE) {
+		// download exported files from tmp folder (if gsdl server is remote)			
+		if (Gatherer.isGsdlRemote) {
+		    if (progress!=null) {
+			progress.messageOnProgressBar("Downloading CD-ROM data from server");
+		    }
+
+		    // !! TO DO
+		    
+ 		    if (progress!=null) {
+ 			progress.messageOnProgressBar("");
+ 		    }
+		}
+	    }
+	}
+
+	// We're done.	
+	//ystem.err.println("Firing process complete for " + type + "...");
+	fireProcessComplete(type, status);
+	// Close bos
+	if(bos != null) {
+	    try {
+		bos.close();
+		bos = null;
+	    }
+	    catch(Exception error) {
+		DebugStream.printStackTrace(error);
+	    }
+	}
+    }
+
+
+    public void fireMessage(String message)
+    {
+	fireMessage(type, typeAsString(type) + "> " + message, status, buffered_output_stream);
+    }
+
+
+    /** Method for firing a message to all interested listeners.
+     * @param type An <strong>int</strong> indicating the process type.
+     * @param message The message as a <strong>String</strong>.
+     * @param status An <strong>int</strong> specifying the current status of the process.
+     */
+    public void fireMessage(int type, String message, int status, BufferedOutputStream bos) {
+	GShellEvent event = new GShellEvent(this, 0, type, message, status);
+	// If there is a progress monitor attached, pass the event to it first. Note that we pass a queue of messages as the processing may cause one message to be split into several.
+	ArrayList message_queue = new ArrayList();
+	message_queue.add(event);
+	if(progress != null) {
+	    progress.process(message_queue);
+	}
+	for(int j = 0; j < message_queue.size(); j++) {
+	    GShellEvent current_event = (GShellEvent) message_queue.get(j);
+	    // If the event hasn't been vetoed, pass it on to other listeners
+	    if(!current_event.isVetoed()) {
+		Object[] concerned = listeners.getListenerList();
+		for(int i = 0; i < concerned.length ; i++) {
+		    if(concerned[i] == GShellListener.class) {
+			((GShellListener)concerned[i+1]).message(current_event);
+		    }
+		}
+		concerned = null;
+	    }
+	}
+	// And if we have a buffered output stream from error messages, send the message there
+	if(bos != null) {
+	    try {
+		bos.write(message.getBytes(), 0, message.length());
+	    }
+	    catch(Exception exception) {
+		DebugStream.println("Exception in GShell.fireMessage() - unexpected");
+		DebugStream.printStackTrace(exception);
+	    }
+	}
+	message_queue = null;
+	event = null;
+    }
+
+    /** Method for firing a process begun event which is called, strangely enough, when the process begins.
+     * @param type An <strong>int</strong> indicating the process type.
+     * @param status An <strong>int</strong> specifying the current status of the process.
+     */
+    protected void fireProcessBegun(int type, int status) {
+	// Start the progres monitor if available
+	if(progress != null) {
+	    //ystem.err.println("About to call progress.start().");
+	    progress.start();
+	    //ystem.err.println("Called progress.start().");
+	}
+	// Fire an event
+	GShellEvent event = new GShellEvent(this, 0, type, "", status);
+	Object[] concerned = listeners.getListenerList();
+	for(int i = 0; i < concerned.length ; i++) {
+	    if(concerned[i] == GShellListener.class) {
+		((GShellListener)concerned[i+1]).processBegun(event);
+	    }
+	}
+    }
+
+
+    /** Method for firing a process complete event which is called, no surprise here, when the process ends.
+     * @param type An <strong>int</strong> indicating the process type.
+     * @param status An <strong>int</strong> specifying the current status of the process.
+     */
+    protected void fireProcessComplete(int type, int status) {
+	// Tidy up by stopping the progress bar. If it was cancelled then the cancel command has arrived via the progress bars and they don't need to be told again (it actually causes problems).
+	if(progress != null && status != CANCELLED) {
+	    progress.stop();
+	}
+	
+	// If we were cancelled, and we are lower details modes, fire off one last message.
+	if(status == CANCELLED && Configuration.getMode() <= Configuration.LIBRARIAN_MODE) {
+	    GShellEvent current_event = new GShellEvent(this, 0, type, Dictionary.get("GShell.Build.BuildCancelled"), status);
+	    Object[] concerned = listeners.getListenerList();
+	    for(int i = 0; i < concerned.length ; i++) {
+		if(concerned[i] == GShellListener.class) {
+		    ((GShellListener)concerned[i+1]).message(current_event);
+		}
+	    }
+	    concerned = null;
+	}
+	
+	String msg = "";
+	// If we are creating collection and have trouble with permissions, we need more messages
+	if(status == ERROR && type == GShell.NEW){
+	  msg = args[args.length-1];
+	}
+	// And firing off an event
+	GShellEvent event = new GShellEvent(this, 0, type, msg, status);
+	Object[] concerned = listeners.getListenerList();
+	for(int i = 0; i < concerned.length ; i++) {
+	    if(concerned[i] == GShellListener.class) {
+		((GShellListener)concerned[i+1]).processComplete(event);
+	    }
+	}
+    }
+
+    /** Method to determine if the user, via the progress monitor, has signalled stop.
+     * @return A <strong>boolean</strong> indicating if the user wanted to stop.
+     */
+    public boolean hasSignalledStop() {
+	boolean has_signalled_stop = false;
+	if(progress != null) {
+	    has_signalled_stop = progress.hasSignalledStop();
+	}
+	if(has_signalled_stop) {
+	    status = CANCELLED;
+	}
+	return has_signalled_stop;
+    }
+
+    /** Converts a type into a text representation.
+     * @param type An <strong>int</strong> which maps to a shell process type.
+     * @return A <strong>String</strong> which is the thread process's text name.
+     */
+    public String typeAsString(int type) {
+	String name = null;
+	switch(type) {
+	case BUILD:
+	    name = "buildcol.pl";
+	    break;
+	case IMPORT:
+	    name = "import.pl";
+	    break;
+	case NEW:
+	    name = "mkcol.pl";
+	    break;
+	case EXPORTAS:
+	    name = "export.pl";
+	    break;
+	case CDIMAGE:
+	    name = "exportcol.pl";
+	    break;
+	case CONVERT:
+	    name = "convert_coll_from_gs2.pl";
+	    break;
+	case EXPLODE:
+	    name = "explode_metadata_database.pl";
+	    break;
+	case SRCREPLACE: // To work with replace_srcdoc_with_html.pl
+	    name = "replace_srcdoc_with_html.pl";
+	    break;
+	case SCHEDULE:
+	    name = "schedule.pl";
+	    break; 
+	default:
+	    name = "";
+	}
+	return name;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellElement.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellElement.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellElement.java	(revision 31635)
@@ -0,0 +1,106 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, New Zealand Digital Library, University of Waikato
+ *
+ * Copyright (C) 2004 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.shell;
+import java.util.HashMap;
+import java.util.StringTokenizer;
+import org.greenstone.gatherer.util.StaticStrings;
+
+/** This class wraps around a pseudo-XML message we have recieved from an external script, and allows access to the element name, attributes etc via method calls. */
+public class GShellElement {
+    private boolean is_closed = false;
+    private HashMap attributes;
+    private String element_name;
+    /** Create a new element around the message string. The string is expected to start with < and end with > */
+    /** this is a hack - why aren't we using proper XML parsing?? */
+    public GShellElement(String message) {
+	// Remove the angle brackets
+	message = message.substring(1, message.length() - 1);
+	// Tokenize the message
+	StringTokenizer tokenizer = new StringTokenizer(message);
+	// The first token is the element name - be aware it may start with the close tag indicator
+	element_name = tokenizer.nextToken();
+	if(element_name.startsWith(StaticStrings.URL_SEPARATOR_CHARACTER)) {
+	    element_name = element_name.substring(1);
+	    is_closed = true;
+	}
+	// Now while we have remaining tokens
+	while(tokenizer.hasMoreTokens()) {
+	    int index = -1;
+	    // The next token might be a attribute=value pair or it could be the close tag indicator
+	    String token = tokenizer.nextToken();
+	    if(token.equals(StaticStrings.URL_SEPARATOR_CHARACTER)) {
+		is_closed = true;
+	    }
+	    else if(token.length() > 3 && (index = token.indexOf(StaticStrings.EQUALS_CHARACTER)) != -1) {
+		String attribute_name = token.substring(0, index);
+		String attribute_value = token.substring(index + 1);
+		// Value may end in a /
+		if(attribute_value.endsWith(StaticStrings.URL_SEPARATOR_CHARACTER)) {
+		    is_closed = true;
+		    attribute_value = attribute_value.substring(0, attribute_value.length() - 1);
+		}
+		// We may have to strip ' marks off value - if it starts with ' and doesn't end with ' we read in more tokens until we get an ending '
+		if(attribute_value.startsWith(StaticStrings.SINGLE_QUOTE_CHARACTER)) {
+		    while (!attribute_value.endsWith(StaticStrings.SINGLE_QUOTE_CHARACTER) && tokenizer.hasMoreTokens()) {
+			attribute_value += " "+tokenizer.nextToken();
+		    }
+		    if (attribute_value.endsWith(StaticStrings.SINGLE_QUOTE_CHARACTER)) {
+			attribute_value = attribute_value.substring(1, attribute_value.length() - 1);
+		    }
+		}
+		if(attributes == null) {
+		    attributes = new HashMap();
+		}
+		attributes.put(attribute_name, attribute_value);
+		attribute_value = null;
+		attribute_name = null;
+	    }
+	    token = null;
+	}
+	tokenizer = null;
+    }
+
+    public String getAttribute(String attribute_name) {
+	String result = null;
+	if(attributes != null) {
+	    result = (String) attributes.get(attribute_name);
+	}
+	if(result == null) {
+	    result = "";
+	}
+	return result;
+    }
+
+    public String getElementName() {
+	return element_name;
+    }
+
+    public boolean isClosed() {
+	return is_closed;
+    }
+}
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellEvent.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellEvent.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellEvent.java	(revision 31635)
@@ -0,0 +1,120 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.shell;
+
+/** Title:        The Gatherer<br>
+ * Description:  The Gatherer: a tool for gathering and enriching digital collections.<br>
+ * Copyright:    Copyright (c) 2001<br>
+ * Company:      The University of Waikato<br>
+ * Written:        /  /01<br>
+ * Revised:      12/05/02 - Commented<br>
+ *               29/05/02 - Moved into correct package<br>
+ * @author John Thompson, Greenstone Digital Libraries
+ * @version 2.1 */
+import java.awt.AWTEvent;
+
+/** This class encapsulates all the information created by an event within a <strong>GShell</strong> process. */
+public class GShellEvent 
+    extends AWTEvent {
+    /** Allows some part of the program to prevent this message being processed by other parts. */
+    private boolean vetoed = false;
+    /** The status of the process at the completion of event. */
+    private int status = -1;
+    /** The process type (such as COPY, BUILD or IMPORT). */
+    private int type = -1;
+    /** Any message associated with this event. */
+    private String message = null;
+
+    /* Constructor.
+     * @param source The <strong>GShell</strong> that fired this message.
+     * @param id The event identifier as an <strong>int</strong>.
+     * @param type The process type as an <strong>int</strong>.
+     * @param message A <strong>String</strong> representing any message attatched with this event.
+     * @param status The status of the process post event, as an <strong>int</strong>.
+     */
+    public GShellEvent(Object source, int id, int type, String message, int status) {
+	super(source, id);
+	this.message = message;
+	this.status = status;
+	this.type = type;
+    }
+
+    /** Gets the message associated with this event.
+     * @return The message as a <strong>String</strong> or <i>null</i>.
+     */
+    public String getMessage() {
+	return message;
+    }
+
+    /** Gets the status associated with this event. This status can then be matched back to the constants in <strong>GShell</strong>.
+     * @return An <strong>int</strong> signifying the process status.
+     */ 
+    public int getStatus() {
+	return status;
+    }
+
+    /** Gets the type associated with this event. This type can then be matched back to the constants in <strong>GShell</strong>.
+     * @return An <strong>int</strong> signifying the process type.
+     */ 
+    public int getType() {
+	return type;
+    }
+
+    /** Determine if this event has been vetoed by some other part of the GLI
+     * @return true if the event has, and thus shouldn't be further processed, false otherwise.
+     */
+    public boolean isVetoed() {
+	return vetoed;
+    }
+
+    /** Change the message to be displayed by this event - usually from something technical so something easier to understand.
+     * @param message the new message as a String
+     */
+    public void setMessage(String message) {
+	this.message = message;
+    }
+
+    public String toString() {
+	return "org.greenstone.gatherer.shell.GShellEvent[" + message + "," + status + "," + type + "]";
+    }
+
+    /** Prevent this event being processed by other parts of the GLI.
+     */
+    public void veto() {
+	vetoed = true;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellListener.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellListener.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellListener.java	(revision 31635)
@@ -0,0 +1,70 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.shell;
+
+/**
+ * Title:        The Gatherer<br>
+ * Description:  The Gatherer: a tool for gathering and enriching digital collections.<br>
+ * Copyright:    Copyright (c) 2001<br>
+ * Company:      The University of Waikato<br>
+ * Written:        /  /01<br>
+ * Revised:      12/05/02 - Commented<br>
+ *               29/05/02 - Moved into correct package<br>
+ * @author John Thompson, Greenstone Digital Libraries
+ * @version 2.1
+ */
+import java.util.EventListener;
+
+/** This interface details the methods required of a class that wishes to listen to a <strong>GShell</strong>.
+ */
+public interface GShellListener 
+    extends EventListener {
+    /** All implementation of <i>GShellListener</i> must include this method so the listener can be informed of messages from the <strong>GShell</strong>.
+     * @param event A <strong>GShellEvent</strong> that contains, amoung other things, the message.
+     */
+    public void message(GShellEvent event);
+
+    /** All implementation of <i>GShellListener</i> must include this method so the listener can be informed when a <strong>GShell</strong> begins its task.
+     * @param event A <strong>GShellEvent</strong> that contains details of the initial state of the <strong>GShell</strong> before task comencement.
+     */
+    public void processBegun(GShellEvent event);
+
+    /** All implementation of <i>GShellListener</i> must include this method so the listener can be informed when a <strong>GShell</strong> completes its task.
+     * @param event A <strong>GShellEvent</strong> that contains details of the final state of the <strong>GShell</strong> after task completion.
+     */
+    public void processComplete(GShellEvent event);
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellProgressMonitor.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellProgressMonitor.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/shell/GShellProgressMonitor.java	(revision 31635)
@@ -0,0 +1,105 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.shell;
+
+/**
+ * Title:        The Gatherer<br>
+ * Description:  The Gatherer: a tool for gathering and enriching digital collections.<br>
+ * Copyright:    Copyright (c) 2001<br>
+ * Company:      The University of Waikato<br>
+ * Written:        /  /01<br>
+ * Revised:      12/05/02 - Commented<br>
+ *               29/05/02 - Moved into correct package<br>
+ * @author John Thompson, Greenstone Digital Libraries
+ * @version 2.1
+ */
+import java.awt.Component;
+import java.util.ArrayList;
+import org.greenstone.gatherer.gui.GProgressBar;
+
+/** When implemented, this interface allows another class to monitor the progress of a <strong>GShell</strong> process. Specifically implementing classes should be designed to take the textual output from the process and then translate that message into some quanitive measure of progress which can then be shown on the progress bar.
+ */
+public interface GShellProgressMonitor {
+    /** Method to register a new progress bar with this monitor.
+     * @param progress_bar The new <strong>GProgressBar</strong>.
+     */
+    public void addProgressBar(GProgressBar progress_bar);
+
+    /** Determine the script exit value according to the progress monitor. This gets around a problem where several script failures actually result with a successful exit value.
+     * @return A <i>int</i> with a value of zero if and only if the script was successful.
+     */
+    public int exitValue();
+
+    public GProgressBar getSharedProgress();
+
+    /** Method to retrieve whatever control is being used as the progress indicator. Usually a <strong>GProgressBar</strong> but there may be others implemented later.
+     * @return A <strong>Component</strong> on which the progress of the process is being displayed.
+     */
+    public Component getProgress();
+
+    public void messageOnProgressBar(String message);
+
+    /** Method to determine the state of the stop flag, which may be set by the visual component that created this monitor.
+     * @return A <strong>boolean</strong> indicating if the process has been asked to stop.
+     */
+    public boolean hasSignalledStop();
+
+    /** Inform the progress bar that it should programatically increment progress by one step. */
+    public void increment();
+
+    /** This method is used to 'feed in' a line of text captured from the process.
+     * @param queue what will be used as a queue of events, but what, at the moment, should only have one event in it
+     */
+    public void process(ArrayList queue);
+
+    public void reset();
+
+    public void saving();
+
+    /** Since the creator of this process monitor is actually in the GUI, this class provides the simpliest way to send a cancel process message between the two.
+     * @param state The desired state of the stop flag as a <strong>boolean</strong>.
+     */
+    public void setStop(boolean state);
+
+    /** This method resets this monitor to the start, reseting the process parsing and progress bar.
+     */
+    public void start();
+
+    /** This method indicates the process is complete. 
+     */
+    public void stop();
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/AppendLineOnlyFileDocument.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/AppendLineOnlyFileDocument.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/AppendLineOnlyFileDocument.java	(revision 31635)
@@ -0,0 +1,1003 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Greenstone Librarian Interface (GLI) application, 
+ * part of the Greenstone digital library software suite from the New 
+ * Zealand Digital Library Project at the University of Waikato, 
+ * New Zealand.
+ *
+ * Author: John Thompson, Greenstone Project, New Zealand Digital Library, 
+ *         University of Waikato
+ *         http://www.nzdl.org
+ *
+ * Copyright (C) 2004 New Zealand Digital Library, University of Waikato
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.util;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.gui.GLIButton;
+
+/** A Document whose underlying data is stored in a RandomAccessFile, and whose Element implementations lack the memory hogging problems associated with anything that extends the AbstractDocument class. This Document, for reasons of time constraints and sanity, only provides an editting ability of appending new lines to the end of the current document, perfect for our logging needs, completely useless for text editing purposes. Furthermore, since the append actions tend to somewhat swamp the IO, I'll temporarily store strings in the structure model, and write them out using a separate thread. 
+ * @author John Thompson, Greenstone Project, New Zealand Digital Library, University of Waikato
+ * @version 2.41 final
+ */
+public class AppendLineOnlyFileDocument
+    implements Document {
+    /** A default string to append at the start of each log file. Of special note is the beginning '.' character that will, upon completion of the import/build process, be replaced with a letter indicating the final status of this build attempt. */
+    static final private String GLI_HEADER_STR = ".";
+    /** The root element of the document model. */
+    private AppendLineOnlyFileDocumentElement root_element;
+    /** The current owner of this document, useful for callbacks when certain tasks are complete. */
+    private AppendLineOnlyFileDocumentOwner owner;
+    /** Is this document a build log, and hence has some extra functionality, of just a straight log. */
+    private boolean build_log = false;
+    /** A list of listeners attached to this document. */
+    private EventListenerList listeners_list;
+    /** This properties hash map allows you to store any metadata about this document for later review. */
+    private HashMap properties;
+    /** Indicates the current length of the underlying file (which may not equal the current number of characters in the document). */
+    private long length;
+    /** The random access file which will back this document model. */
+    private RandomAccessFile file;
+    /** The filename given to the underlying file. */
+    private String filename;
+    /** An independant thread responsible for writing the 'in memory' contents of the document model to the random access file as appropriate. This is done so IO lag does not effect the gui as much as it used to. */
+    private WriterThread writer;
+
+    
+    public AppendLineOnlyFileDocument(String filename) {
+	this(filename, true);
+    }
+
+    /** The constructor is responsible for creating several necessary data structures as well as opening the random access file. When it creates the new file, as it is wont to do, it will immediately add the special header line defined above.
+     * @param filename the absolute path name of the file as a String
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread
+     * @see org.greenstone.gatherer.util.StaticStrings#NEW_LINE_CHAR
+     */
+    public AppendLineOnlyFileDocument(String filename, boolean build_log) {
+	DebugStream.println("Creating log: " + filename);
+	// Initialization 
+	this.build_log = build_log;
+	this.filename = filename;
+	this.listeners_list = new EventListenerList();
+	this.properties = new HashMap();
+	this.writer = new WriterThread();
+	writer.start();
+	// Open underlying file
+	try {
+	    file = new RandomAccessFile(filename, "rw");
+	    // Create the root element.
+	    length = file.length();
+	    root_element = new AppendLineOnlyFileDocumentElement();
+	    // Now quickly read through the underlying file, building an Element for each line.
+	    long start_offset = 0L;
+	    file.seek(start_offset);
+	    int character = -1;
+	    while((character = file.read()) != -1) {
+		if(character == StaticStrings.NEW_LINE_CHAR) {
+		    long end_offset = file.getFilePointer();
+		    Element child_element = new AppendLineOnlyFileDocumentElement(start_offset, end_offset);
+		    root_element.add(child_element);
+		    child_element = null;
+		    start_offset = end_offset;
+		}
+	    }
+	    // If there we no lines found, and we are a build log, then append the file header.
+	    if(build_log && root_element.getElementCount() == 0) {
+		appendLine(GLI_HEADER_STR);
+	    }
+	}
+	catch (Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+    }
+
+    /** Adds a document listener for notification of any changes. 
+     * @param listener the new DocumentListener to keep track of
+     */
+    public void addDocumentListener(DocumentListener listener) {
+	///ystem.err.println("addDocumentListener(" + listener + ")");
+	listeners_list.add(DocumentListener.class, listener);
+    }
+
+    /** Append some content after the document. 
+     * @param str the String to add as a new line in the document
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentEvent
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread#queue
+     */
+    public void appendLine(String str) {
+	// Ensure the string ends in a newline
+	try {
+	    if (!str.endsWith("\n")) {
+		str = str + "\n";
+	    }
+	    int str_length = str.getBytes("UTF-8").length;
+	    long start_offset = length;
+	    long end_offset = start_offset + (long) str_length;
+	    length = length + str_length;
+	    // Create a new element to represent this line
+	    AppendLineOnlyFileDocumentElement new_line_element = new AppendLineOnlyFileDocumentElement(start_offset, end_offset, str);
+	    root_element.add(new_line_element);
+	    // Queue the content to be written.
+	    writer.queue(new_line_element);
+	    // Now fire an event so everyone knows the content has changed.
+	    DocumentEvent event = new AppendLineOnlyFileDocumentEvent(new_line_element, (int)start_offset, str_length, DocumentEvent.EventType.INSERT);
+	    Object[] listeners = listeners_list.getListenerList();
+	    for (int i = listeners.length - 2; i >= 0; i = i - 2) {
+		if (listeners[i] == DocumentListener.class) {
+		    ((DocumentListener)listeners[i+1]).insertUpdate(event);
+		}
+	    }
+	    listeners = null;
+	    event = null;
+	    new_line_element = null;
+	}
+	catch(Exception error) {
+	    DebugStream.printStackTrace(error);
+	}
+    }
+
+    /** Returns a position that will track change as the document is altered. 
+     * @param offs the position offset as an int
+     * @return the Position to be monitored
+     *  @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentPosition
+     */
+    public Position createPosition(int offs)  {
+	return new AppendLineOnlyFileDocumentPosition(offs);
+    }
+
+	/** ensure the current build_log file has finished transferring from memory to disk, and that the random access
+	* file is properly closed when a collection is closed.  Else we can't delete the just-closed collection since the 
+	* build_log file resource is still kept open. */
+	public void close() {
+		if(writer == null) { // closed already
+			return;
+		}
+		// get rid of the writer and then close the file
+		setExit();
+		writer = null;
+		try {	
+			file.close();
+		} catch(Exception exception) {
+			DebugStream.println("Exception in AppendLineOnlyFileDocument.close() - Unable to close internal file");
+			DebugStream.printStackTrace(exception);
+		}
+	}
+	
+    /** The destructor is implemented to ensure the current log file has finished transferring from memory to disk, and that the random access file is properly closed, before GLI exits.
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread#finish
+     */
+    public void destroy() {
+	try {
+		if(writer != null) {
+			writer.finish();
+			writer = null;
+			file.close();
+		}
+	}
+	catch(Exception exception) {
+	    DebugStream.println("Exception in AppendLineOnlyFileDocument.destroy() - unexpected");
+	    DebugStream.printStackTrace(exception);
+	}
+    }
+
+    /** Gets the default root element for the document model. 
+     * @return the root Element of this document
+     */
+    public Element getDefaultRootElement() {
+	return root_element;
+    }
+
+    /** Returns the length of the data. 
+     * @return the length as an int (not a long)
+     */
+    public int getLength() {
+	return (int) length;
+    }
+
+    /** A version of get length which returns the offset to the start of the last line in the document.
+     * @return the offset length as an int
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement
+     */
+    public int getLengthToNearestLine() {
+	AppendLineOnlyFileDocumentElement last_element = (AppendLineOnlyFileDocumentElement)root_element.getElement(root_element.getElementCount() - 1);
+	if(last_element != null ) {
+	    return last_element.getStartOffset();
+	}
+	else {
+	    return (int) length; // The best we can do.
+	}
+    }
+	
+    /** Retrieve a property that has previously been set for this document.
+     * @param key the key String under which the vaue is stored
+     * @return the value which is also a String, or null if no such value
+     */
+    public Object getProperty(Object key) {
+	return properties.get(key);
+    }
+
+    /** Determine if the writer thread is still running, ie a log file which is currently open for writing
+     * @return true if the writer is still active, false otherwise
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread#isStillWriting
+     */
+    public boolean isStillWriting() {
+	return writer.isStillWriting();
+    }
+
+    /** Gets a sequence of text from the document. 
+     * @param offset the starting offset for the fragment of text required, as an int
+     * @param l the length of the text, also as an int
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement
+     * @see org.greenstone.gatherer.util.StaticStrings#EMPTY_STR
+     */
+    public String getText(int offset, int l) 
+	throws BadLocationException {
+
+	if (l == 0) {
+	    return "";
+	}
+
+	try {
+	    return read((long) offset, l);
+	}
+	catch (Exception ex) {
+	}
+
+	return "";
+
+//  	// System.err.println("AppendLineOnlyFileDocument::getText() request...");
+//  	String request = "getText(" + offset + ", " + l + ")";
+//  	///ystem.err.println(request);
+//  	String text = null;
+//  	if(text == null || text.length() < l) {
+//  	    try {
+//  		int file_length = (int) file.length();
+//  		///ystem.err.println("file_length = " + file_length + ", length = " + length);
+//  		if(l == 0) {
+//  		    text = StaticStrings.EMPTY_STR;
+//  		}
+//  		else if(0 <= offset && offset < length && (offset + l) <= length) {
+//  		    if(offset < file_length) {
+//  			text = read((long)offset, l);
+//  			if(text.length() != l) {
+//  			    ///ystem.err.println("Asked for " + l + " characters of text. But only read " + text.length());
+//  			    throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset); 
+//  			}
+//  		    }
+//  		    else {
+//  			int index = root_element.getElementIndex(offset);
+//  			if(index < root_element.getElementCount()) {
+//  			    AppendLineOnlyFileDocumentElement element = (AppendLineOnlyFileDocumentElement) root_element.getElement(index);
+//  			    text = element.getContent();
+//  			}
+//  			else {
+//  			    ///ystem.err.println("Index is " + index + " but there are only " + root_element.getElementCount() + " children.");
+//  			    throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset); 
+//  			}
+//  		    }
+		    
+//  		}
+//  	    }
+//  	    catch (IOException error) {
+//  		DebugStream.printStackTrace(error);
+//  	    }
+//  	    if(text == null) {
+//  		///ystem.err.println("Text is null.");
+//  		throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset);
+//  	    }
+//  	}
+//  	request = null;
+//  	return text;
+    }
+
+    /** Fetches the text contained within the given portion of the document. 
+     * @param offset the starting offset of the desired text fragment, as an int
+     * @param length the length of the text also as an int
+     * @param txt the Segment into which the text fragment should be placed
+     */
+    public void getText(int offset, int length, Segment txt) 
+	throws javax.swing.text.BadLocationException  {
+	String str = getText(offset, length);
+	txt.array = str.toCharArray();
+	txt.count = str.length();
+	txt.offset = 0;
+	str = null;
+    }
+	
+    /** Set a property for this document.
+     * @param key the key to store the value under, as a String
+     * @param value the property value itself also as a String
+     */
+    public void putProperty(Object key, Object value) {
+	properties.put(key, value);
+    }
+
+    /** Determine if this document is ready by testing if it has an open descriptor to the random access file.
+     * @return true if the file exists thus the document is ready, false otherwise
+     */
+    public boolean ready() {
+	return (file != null);
+    }
+	
+    /** Removes a document listener.
+     * @param listener the Document we wish to remove
+     */
+    public void removeDocumentListener(DocumentListener listener) {
+	listeners_list.remove(DocumentListener.class, listener);
+    }
+
+    /** Whenever the user requests a change of the loaded log, this method is called to ensure the previous log has been completely written to file. Note that this only garuentees that text fragments currently queued for writing will be correctly written - while the behaviour is undefined for text fragments added after this call (some may write depending on several race conditions).
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread#exit
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread#finish
+     */
+    public void setExit() {
+	writer.exit();
+	writer.finish();
+    }
+
+    /** Establish the current owner of this document.
+     * @param owner the current AppendLineOnlyFileDocumentOwner
+     */
+    public void setOwner(AppendLineOnlyFileDocumentOwner owner) {
+	this.owner = owner;
+    }
+
+    /** To record the final state of the logging process we reserve a single character at the start of the file, and then replace it when the build process is complete.
+     * @param character the final status char to replace the X at the start of the log
+     */
+    public synchronized void setSpecialCharacter(char character) {
+	if(build_log) {
+	    try {
+		file.seek(0L);
+		file.write((int)character);
+	    }
+	    catch (Exception error) {
+		DebugStream.printStackTrace(error);
+	    }
+	}
+    }
+
+    /** Request a text representation of this document which currently returns the filename.
+     * @return the text as a String
+     */
+    public String toString() {
+	return filename;
+    }
+
+    /** This method reads a fragment of text from the underlying random access file. It is synchronized so that the file isn't being written to at the same time.
+     * @param start_offset the offset within the file to set the read pointer at, as a long
+     * @param l the number of characters to read as an int
+     */
+    private synchronized String read(long start_offset, int l) 
+	throws IOException {
+	//print("read(" + start_offset + ", " + l + ")... ");
+	byte[] buffer = new byte[l];
+	file.seek(start_offset);
+	int result = file.read(buffer, 0, l);
+	return new String(buffer, "UTF-8");
+	//print("read() complete");
+    }
+
+    /** This methods writes a fragment of text to the underlying random access file. It is synchronized so that the file isn't being read from at the same time.
+     * @param start_offset the offset within the file to set the write pointer at, as a long
+     * @param end_offset the final offset of the new text fragment within the file, as a long. This is used to extend the file to be the correct length
+     * @param str the String to be written
+     * @param l the number of characters from str to be written, as an int
+     */
+    private synchronized void write(long start_offset, long end_offset, String str, int l) 
+	throws IOException {	
+	//print("write(" + start_offset + ", " + end_offset + ", " + str + ", " + l + ")");
+	file.setLength(end_offset);
+	file.seek(start_offset);
+	file.write(str.getBytes("UTF-8"), 0, l);
+	//print("write() complete");
+    }
+
+    /** This class provides the building-block for the document model. Each element is a line in the document, or has no content but contains several child elements. Due to the basic nature of what we are modelling only the root node has children. */
+    private class AppendLineOnlyFileDocumentElement
+	extends ArrayList
+	implements Element {
+	/** Our parent Element. */
+	private Element parent;
+	/** The offset to the end of our fragment(s) of text, in respect to the entire document. */
+	private long end;
+	/** The offset to the start of our fragment(s) of text, in respect to the entire document. */
+	private long start;
+	/** If we haven't been written to file yet, this contains the text fragment itself. */
+	private String content;
+
+	/** Construct a new root element, which can have no content, but calculates its start and end from its children. */
+	public AppendLineOnlyFileDocumentElement() {
+	    super();
+	    this.end = 0L;
+	    this.parent = null;
+	    this.start = 0L;
+	}
+
+	/** Construct a new element, whose content is found in bytes start to end - 1 within the random access file backing this document.
+	 * @param start the starting offset as a long
+	 * @param end the ending offset as a long
+	 */
+	public AppendLineOnlyFileDocumentElement(long start, long end) {
+	    super();
+	    this.end = end;
+	    this.parent = null;
+	    this.start = start;
+	}
+
+	/** Construct a new element, whose content is provided, but should at some later time be written to bytes start to end - 1 in the random access file backing this document.
+	 * @param start the starting offset as a long
+	 * @param end the ending offset as a long
+	 * @param content the text fragment String which we represent in the model
+	 */
+	public AppendLineOnlyFileDocumentElement(long start, long end, String content) {
+	    super();
+	    this.content = content;
+	    this.end = end;
+	    this.parent = null;
+	    this.start = start;
+	}
+
+	/** Add the given node as one of our children.
+	 * @param child the new child Element
+	 */
+	public void add(Element child) {
+	    super.add(child);
+	    ((AppendLineOnlyFileDocumentElement)child).setParent(this);
+	}
+
+	/** Having written the content to file this method removes it from the element. */ 
+	public void clearContent() {
+	    content = null;
+	}
+
+	/** This document does not allow content markup. 
+	 * @return always returns null
+	 */
+	public AttributeSet getAttributes() {
+	    return null;
+	}
+
+	/** Retrieve the current content of this element, which may be the text fragment this element represents.
+	 * @return either the text fragment as a String, or null if the fragment has already been written to disk
+	 */
+	public String getContent() {
+	    return content;
+	}
+
+	/** Fetches the document associated with this element. 
+	 * @return the AppendLineOnlyDocument containing this element
+	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument
+	 */
+	public Document getDocument() {
+	    return AppendLineOnlyFileDocument.this;
+	}
+
+	/** Fetches the child element at the given index. 
+	 * @param index the int index of the element to retrieve
+	 * @return the requested Element
+	 */
+	public Element getElement(int index) {
+	    Element element;
+	    if(0 <= index && index < size()) {
+		element = (Element) get(index);
+	    }
+	    else {
+		throw new IndexOutOfBoundsException("AppendLineOnlyDocument.AppendLineOnlyFileDocumentElement.getElement(" + index + ")");
+	    }
+	    return element;
+	}
+          
+	/** Gets the number of child elements contained by this element. 
+	 * @return an int
+	 */
+	public int getElementCount() {
+	    return size();
+	}
+
+	/** Gets the child element index closest to the given offset.
+	 * @param offset the offset within the text of the document, which due to the way the model is built must fll within the bounds of one of our child nodes
+	 * @return the closest index as an int
+	 */
+	public int getElementIndex(int offset) {
+	    int index = -1;
+	    if(parent != null) {
+		index = -1;
+	    }
+	    else if(offset < 0) {
+		index = 0;
+	    }
+	    else if(offset >= length) {
+		index = size() - 1;
+	    }
+	    else {
+		int size = size();
+		for(int i = 0; index == -1 && i < size; i++) {
+		    Element child = (Element) get(i);
+		    if(child.getStartOffset() <= offset && offset < child.getEndOffset()) {
+			index = i;
+		    }
+		    child = null;
+		}
+	    }
+	    return index;
+	}
+          
+	/** Fetches the offset from the beginning of the document that this element ends at. 
+	 * @return the offset as an int
+	 */
+	public int getEndOffset() {
+	    if(parent != null) {
+		return (int) end;
+	    }
+	    // Return the Documents length.
+	    else {
+		return (int) length;
+	    }
+	}
+          
+	/** This method retrieves the name of the element, however names are not important in this document so the name is always an empty string.
+	 * @return an empty String
+	 * @see org.greenstone.gatherer.util.StaticStrings#EMPTY_STR
+	 */
+	public String getName() {
+	    return StaticStrings.EMPTY_STR;
+	}
+          
+	/** Fetches the parent element. 
+	 * @return the parent Element
+	 */
+	public Element getParentElement() {
+	    return parent;
+	}
+          
+	/** Fetches the offset from the beginning of the document that this element begins at. 
+	 * @return the offset as an int
+	 */
+	public int getStartOffset() {
+	    if(parent != null) {
+		return (int) start;
+	    }
+	    else {
+		return 0;
+	    }
+	}
+          
+	/** Since this is a very simple model, only the root node can have children. All the children are leaves. 
+	 * @return true if this is the root node, false otherwise
+	 */
+	public boolean isLeaf() {
+	    return (parent != null);
+	} 
+
+	/** Establish the parent of this element
+	 * @param parent the parent Element
+	 */
+	public void setParent(Element parent) {
+	    this.parent = parent;
+	}
+    }
+    // AppendLineOnlyFileDocumentElement
+
+    /** This event is created to encapsulate the details of some change to the document. */
+    private class AppendLineOnlyFileDocumentEvent
+	implements DocumentEvent {
+	/** The type of change event. */
+	private DocumentEvent.EventType type;
+	/** The element this change occured to, or in. */
+	private AppendLineOnlyFileDocumentElement element;
+	/** Another encapsulating class which contains further detail about the change itself. */
+	private AppendLineOnlyFileDocumentElementChange element_change;
+	/** The length of the text fragment affected by the change. */
+	private int len;
+	/** The offset within the document of the start of the change. */
+	private int offset;
+	
+	/** Construct a new AppendLineOnlyFileDocumentEvent given the pertinant details.
+	 * @param element the AppendLineOnlyFileDocumentElement affected by this change
+	 * @param offset the offset within the document of the start of the change
+	 * @param len the length of the text fragment affected by the change
+	 * @param type the type of change event
+	 */
+	public AppendLineOnlyFileDocumentEvent(AppendLineOnlyFileDocumentElement element, int offset, int len, DocumentEvent.EventType type) {
+	    this.element = element;
+	    this.element_change = null;
+	    this.len = len;
+	    this.offset = offset;
+	    this.type = type;
+	}
+
+	/** Retrieve the docment which generated this event.
+	 * @return the Document
+	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument
+	 */
+	public Document getDocument() {
+	    return AppendLineOnlyFileDocument.this;
+	}
+
+	/** Retrieve the length of the change.
+	 * @return the length as an int
+	 */
+	public int getLength() {
+	    return len;
+	}
+
+	/** Retrieve the start offset of the change.
+	 * @return the offset as an int
+	 */
+	public int getOffset() {
+	    return offset;
+	}
+
+	/** Retrieve the type of change.
+	 * @return the type as a DocumentEvent.EventType
+	 */
+	public DocumentEvent.EventType getType() {
+	    return type;
+	}
+
+	/** Retrieve the element change object which further describes the changes. Of course given that we only allow the appending of new lines there really is only one type of event.
+	 * @param elem implementation side-effect
+	 * @return a DocumentEvent.ElementChange
+	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentEvent.AppendLineOnlyFileDocumentElementChange
+	 */
+	public DocumentEvent.ElementChange getChange(Element elem) {
+	    if(element_change == null) {
+		element_change = new AppendLineOnlyFileDocumentElementChange();
+	    }
+	    return element_change;
+	}
+
+	/** A pretty unexciting implementation of the document element change class. This usually describes the myriad of possible change events that can occur on a document. However we only allow line appends so there is really only one type of change. */
+	private class AppendLineOnlyFileDocumentElementChange
+	    implements DocumentEvent.ElementChange {
+	    /** This array always contains the one Element that was appended. */ 
+	    private Element[] children_added;
+	    /** This Element array is always empty. */
+	    private Element[] children_removed;
+	    /** The index of the affected element within its parents children. */
+	    private int index;
+	    /** Construct a new AppendLineOnlyFileDocumentElementChange with the default 'append line' details. */
+	    public AppendLineOnlyFileDocumentElementChange() {
+		children_added = new Element[1];
+		children_added[0] = element;
+		children_removed = new Element[0];
+		index = root_element.indexOf(element);
+	    }
+
+	    /** Gets the child element that was appended to the parent element. 
+	     * @return an Element[] containing the added element
+	     */
+	    public Element[] getChildrenAdded() {
+		return children_added;
+	    }
+
+	    /** This model does not allow elements to be removed.
+	     * @return an Element[] containing nothing
+	     */
+	    public Element[] getChildrenRemoved() {
+		return children_removed;
+	    }
+          
+	    /** Returns the root element, as our document structure is only two layers deep.
+	     * @return the root Element
+	     */
+	    public Element getElement() {
+		return root_element;
+	    }
+          
+	    /** Fetches the index within the element represented. 
+	     * @return an int specifying the index of change within the root element
+	     */
+	    public int getIndex() {
+		return index;
+	    }
+	}
+	// AppendLineOnlyFileDocumentElementChange
+    }
+    // AppendLineOnlyFileDocumentEvent
+    
+    /** This class denoted a position within our document by an offset. Quite possibly the last interesting class I've ever had to deal with, but you have to implement it so here goes... */
+    private class AppendLineOnlyFileDocumentPosition
+	implements Position {
+	/** The offset within our document. */
+	private int offset;
+	/** Construct a new position given an offset.
+	 * @param offset the offset as an int
+	 */
+	public AppendLineOnlyFileDocumentPosition(int offset) {
+	    this.offset = offset;
+	}
+	/** Retrieve the offset of this position.
+	 * @return the offset as an int
+	 */
+	public int getOffset() {
+	    return offset;
+	}
+    }
+    // AppendLineOnlyFileDocumentPosition
+    
+    /** This thread is responsible to writing the in memory text fragment contents of the document elements out to the random access file. The writing proceedure is implemented this way so that the gui doesn't take the performance hit of the IO. */
+    private class WriterThread
+	extends Thread {
+	/** When true was are being told to assume the contents of the queue are now static, and once completed the queue will be empty (and not expecting any new jobs). Used to finish the queue before the GLI exits. */
+	private boolean empty_queue;
+	/** Setting this flag to true tells the writer to stop regardless of whether there are items left on the queue. */
+	private boolean exit;
+	/** When the writer thread is busy writing content to the file this flag is set to true. */
+	private boolean running;
+	/** The fragment queue as a Vector (for thread reasons). */
+	private Vector queue;
+	/** Construct a new writer thread with an initially empty queue. */
+	public WriterThread() {
+	    super("WriterThread");
+	    empty_queue = false;
+	    exit = false;
+	    queue = new Vector();
+	}
+
+	/** This method is called to ask the writer thread to die after it finishes any current write proceceedure. */
+	public synchronized void exit() {
+	    //print("WriterThread.exit() start");
+	    exit = true;
+	    notify();
+	    //print("WriterThread.exit() complete");
+	}
+
+	/** When this returns there are no jobs waiting in the queue. */
+	public void finish() {
+	    if(!queue.isEmpty() && running) {
+		///ystem.err.println("Somethings gone wrong, as there are still " + queue.size() + " items to be written out and yet the thread seems to be waiting.");
+		empty_queue = true;
+		run();
+	    }
+	}
+
+	/** Determine if the writer is currently running.
+	 * @return true if it is, false if it has been exited or if it was never running in the first place (an old log)
+	 */
+	public boolean isStillWriting() {
+	    return running;
+	}
+
+	/** The run method of this thread checks if there are any document elements queued to be written to file, and then writes them as necessary.
+	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument
+	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement
+	 */
+	public void run() {
+	    exit = false;
+	    running = true;
+	    while(!exit) {
+		if(!queue.isEmpty()) { 
+		    AppendLineOnlyFileDocumentElement element = (AppendLineOnlyFileDocumentElement) queue.remove(0);
+		    // Write the content to file.
+		    String content = element.getContent();
+		    if(content != null) {
+			try {
+			    write(element.getStartOffset(), element.getEndOffset(), content, content.getBytes("UTF-8").length);
+			}
+			catch(Exception error) {
+			    DebugStream.printStackTrace(error);
+			}
+			element.clearContent();
+		    }
+		}
+		else if(empty_queue) {
+		    exit = true;
+		}
+		else {
+		    synchronized(this) {
+			try {
+			    print("WriterThread.wait() start");
+			    wait();
+			}
+			catch(Exception exception) {
+			}
+			print("WriterThread.wait() complete");
+		   }
+		}
+	    }
+	    print("WriterThread completely finished. Exiting.");
+	    running = false;
+	    if(owner != null) {
+		owner.remove(AppendLineOnlyFileDocument.this);
+	    }
+	}
+
+	/** Enqueue a document element to have its text content written to file.
+	 * @param element the Element needing its content written out
+	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement#getContent
+	 */
+	public synchronized void queue(Element element) {
+	    print("WriterThread.queue(): " + ((AppendLineOnlyFileDocumentElement)element).getContent());
+	    queue.add(element);
+	    notify();
+	}
+    }
+    // WriterThread
+
+    // ***** METHODS WE ARE NOW IGNORING BECAUSE WE ARE VIRTUALLY READ-ONLY *****
+    
+    /** Not implemented. Add a new undo listener to this document.
+     * @param listener UndoableEditListener
+     */
+    public void addUndoableEditListener(UndoableEditListener listener) {}
+    	
+    /** Not implemented. Get the last position in the document.
+     * @return null
+     */
+    public Position getEndPosition() { return null; }
+
+    /** Not implemented. Gets all root elements defined.
+     * @return null
+     */
+    public Element[] getRootElements() { return null; }
+	
+    /** Not implemented. Retrieve the first position in the document.
+     * @return null
+     */
+    public Position getStartPosition() { return null; }
+    
+    /** Not implemented. Insert a text fragment into our document.
+     * @param offset int
+     * @param str String
+     * @param a AttributeSet
+     */
+    public void insertString(int offset, String str, AttributeSet a) {}
+
+    /** Not implemented. Removes some content from the document. 
+     * @param offs int
+     * @param len int
+     */
+    public void remove(int offs, int len) {}
+
+    /** Not implemented. Removes an undo listener. 
+     * @param listener UndoableEditListener
+     */
+    public void removeUndoableEditListener(UndoableEditListener listener) {}
+    
+    /** Not implemented. Renders a runnable apparently - whatever that means. 
+     * @param r Runnable
+     */
+    public void render(Runnable r) {}
+
+    // DEBUG ONLY
+
+    /** Print a message to the GLI debug file stream.
+     * @param message the message to be written as a String
+     */
+    static synchronized public void print(String message) {
+	if (message.endsWith("\n")) {
+	    DebugStream.print(message); 
+	}
+	else {
+	    DebugStream.println(message); 
+	}
+    }
+
+    /** Create a test document with the ability to add new lines of text as necessary.
+     * @param args the initial starting arguments as a String array
+     * @see org.greenstone.gatherer.gui.GLIButton
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument
+     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.ReadButtonListener
+     */
+    static public void main(String[] args) {
+	JFrame frame = new JFrame("AppendLineOnlyFileDocument Test");
+	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+	frame.setSize(640,480);
+	JPanel content = (JPanel) frame.getContentPane();
+	
+	//PlainDocument document = new PlainDocument();
+	//document.setAsynchronousLoadPriority(-1);
+	final AppendLineOnlyFileDocument document = new AppendLineOnlyFileDocument("temp.txt");
+
+	final JTextArea text_area = new JTextArea(document);
+
+	JButton read_button = new GLIButton("Read Huge File");
+	read_button.addActionListener(new ReadButtonListener(document));
+	content.setLayout(new BorderLayout());
+	content.add(new JScrollPane(text_area), BorderLayout.CENTER);
+	content.add(read_button, BorderLayout.SOUTH);
+
+	frame.setVisible(true);
+    }
+
+    /** Listens for actions on the read button, and if detected creates a new ReadTask to test the document. 
+     * @author John Thompson, Greenstone Project, New Zealand Digital Library, University of Waikato
+     * @version 2.41 final
+     */
+    static private class ReadButtonListener
+	implements ActionListener {
+	/** The AppendLineOnlyFileDocument object we are testing. */
+	private AppendLineOnlyFileDocument document;
+
+	/** All the constructor does is take a copy of the document to test.
+	 * @param document the Document
+	 */
+	public ReadButtonListener(AppendLineOnlyFileDocument document) {
+	    this.document = document;
+	}
+
+	/** When the button is clicked this method is called to create the new task and run it.
+	 * @param event an ActionEvent containing further information about the button click
+	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.ReadTask
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    Thread task = new ReadTask(document);
+	    task.start();
+	}
+    }
+
+    /** This threaded task opens a large document, aptly named 'big.txt', and then bombards the document object we are testing with lines from the file. This file should be several megs (such as Alice or TREC) to fully test functionality, thread conditions etc. 
+     * @author John Thompson, Greenstone Project, New Zealand Digital Library, University of Waikato
+     * @version 2.41 final
+     */ 
+    static private class ReadTask
+	extends Thread {
+	/** The AppendLineOnlyFileDocument object we are testing. */
+	private AppendLineOnlyFileDocument document;
+
+	/** Construct a new task which will perform testing on the given document.
+	 * @param document the AppendLineOnlyFileDocument to append lines to
+	 */
+	public ReadTask(AppendLineOnlyFileDocument document) {
+	    super("LoadHugeFileThread");
+	    this.document = document;
+	}
+
+	/** The runable part of this class opens the file, then reads it in a line at a time, appending these lines to the ever growing document. */
+	public void run() {
+	    // Load the specified document
+	    try {
+		BufferedReader in = new BufferedReader(new FileReader(new File("big.txt")));
+		String line;
+		
+		while ((line = in.readLine()) != null) {
+		    document.appendLine(line);
+		    try {
+			// Wait a random ammount of time.
+			synchronized(this) {
+			    //print("LoadHugeFileThread.wait() start");
+			    wait(100);
+			    //print("LoadHugeFileThread.wait() complete");
+			}
+		    }
+		    catch(Exception error) {
+			error.printStackTrace();
+		    }
+		}
+		in.close();
+	    }
+	    catch (Exception error) {
+		error.printStackTrace();
+	    }
+	}
+    }
+    // ReadTask
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/AppendLineOnlyFileDocumentOwner.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/AppendLineOnlyFileDocumentOwner.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/AppendLineOnlyFileDocumentOwner.java	(revision 31635)
@@ -0,0 +1,46 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Greenstone Librarian Interface (GLI) application, 
+ * part of the Greenstone digital library software suite from the New 
+ * Zealand Digital Library Project at the University of Waikato, 
+ * New Zealand.
+ *
+ * Author: John Thompson
+ *         Greenstone Project, New Zealand Digital Library
+ *         University of Waikato
+ *         http://www.nzdl.org
+ *
+ * Copyright (C) 2004 New Zealand Digital Library, University of Waikato
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+
+
+/** Any class that wants to act as the owner of an AppendLineOnlyFileDocument has to implement this.
+ * @author John Thompson, Greenstone Project, New Zealand Digital Library, University of Waikato
+ * @version 2.41 final
+ */
+public interface AppendLineOnlyFileDocumentOwner {
+    /** Remove the given document from the owner. The is called by the document itself when it no longer needs a hanging reference in the owner class.
+     * @param document the AppendLineOnlyFileDocument to remove
+     */
+    public void remove(AppendLineOnlyFileDocument document);
+}
+
+
+
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/ArrayTools.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/ArrayTools.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/ArrayTools.java	(revision 31635)
@@ -0,0 +1,341 @@
+/* GPL_HEADER */
+package org.greenstone.gatherer.util;
+
+/**************************************************************************************
+ * Title:        Gatherer
+ * Description:  The Gatherer: a tool for gathering and enriching a digital collection.
+ * Company:      The University of Waikato
+ * Written:        /  /01
+ * Revised:      16/08/02 Improved
+ **************************************************************************************/
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.filechooser.*;
+import org.w3c.dom.*;
+
+/** This utility class contains a series of static methods for common array type manipulations including appending, casting and changing between containers.
+ * @author John Thompson
+ * @version 2.3
+ */
+public class ArrayTools {
+
+    /** Append a Component onto an array of Components. */
+    static public Component[] add(Component[] a, Component b) {
+	Component[] c = null;
+	if(a != null && b != null) {
+	    c = new Component[a.length + 1];
+	    System.arraycopy(a, 0, c, 0, a.length);
+	    c[c.length - 1] = b;
+	}
+	else if(a == null && b != null) {
+	    c = new Component[1];
+	    c[0] = b;
+	}
+	else if(a != null && b == null) {
+	    c = a;
+	}
+	return c;
+    }
+    /** This method efficiently appends a new element onto the end of a element array.
+     * @param a The initial <strong>Element[]</strong>.
+     * @param b The new <strong>Element</strong>.
+     * @return An <strong>Element[]</strong> containing a followed by b.
+     */
+    static public Element[] add(Element a[], Element b) {
+	Element[] c = null;
+	if(a != null && b != null) {
+	    c = new Element[a.length + 1];
+	    System.arraycopy(a, 0, c, 0, a.length);
+	    c[c.length - 1] = b;
+	}
+	else if(a == null && b != null) {
+	    c = new Element[1];
+	    c[0] = b;
+	}
+	else if(a != null && b == null) {
+	    c = a;
+	}
+	return c;
+    }
+    /** This method efficently appends one element array onto the end of another.
+     * @param a The initial <strong>Element[]</strong>.
+     * @param b The <strong>Element[]</strong> to append.
+     * @return An <strong>Element[]</strong> containing a followed by b.
+     */
+    static public Element[] add(Element a[], Element b[]) {
+	Element[] c = null;
+	if(a != null && b != null) {
+	    c = new Element[a.length + b.length];
+	    System.arraycopy(a, 0, c, 0, a.length);
+	    System.arraycopy(b, 0, c, a.length, b.length);
+	}
+	else if(a == null && b != null) {
+	    c = b;
+	}
+	else if(a != null && b == null) {
+	    c = a;
+	}
+	return c;
+    }
+    /** This method efficiently appends a new file onto the end of a file array.
+     * @param a The initial <strong>File[]</strong>.
+     * @param b The new <strong>File</strong>.
+     * @return A <strong>File[]</strong> containing a followed by b.
+     */
+    static public File[] add(File a[], File b) {
+	File[] c = null;
+	if(a != null && b != null) {
+	    c = new File[a.length + 1];
+	    System.arraycopy(a, 0, c, 0, a.length);
+	    c[c.length - 1] = b;
+	}
+	else if(a == null && b != null) {
+	    c = new File[1];
+	    c[0] = b;
+	}
+	else if(a != null && b == null) {
+	    c = a;
+	}
+	return c;
+    }
+    /** This method efficently appends one file array onto the end of another.
+     * @param a The initial <strong>File[]</strong>.
+     * @param b The <strong>File[]</strong> to append.
+     * @return A <strong>File[]</strong> containing a followed by b.
+     */
+    static public File[] add(File a[], File b[]) {
+	File[] c = null;
+	if(a != null && b != null) {
+	    c = new File[a.length + b.length];
+	    System.arraycopy(a, 0, c, 0, a.length);
+	    System.arraycopy(b, 0, c, a.length, b.length);
+	}
+	else if(a == null && b != null) {
+	    c = b;
+	}
+	else if(a != null && b == null) {
+	    c = a;
+	}
+	return c;
+    }
+
+
+    /** This method efficiently appends a new node onto the end of a node array.
+     * @param a The initial <strong>Node[]</strong>.
+     * @param b The new <strong>Node</strong>.
+     * @return A <strong>Node[]</strong> containing a followed by b.
+     */
+    static public Node[] add(Node a[], Node b) {
+	Node[] c = null;
+	if(a != null && b != null) {
+	    c = new Node[a.length + 1];
+	    System.arraycopy(a, 0, c, 0, a.length);
+	    c[c.length - 1] = b;
+	}
+	else if(a == null && b != null) {
+	    c = new Node[1];
+	    c[0] = b;
+	}
+	else if(a != null && b == null) {
+	    c = a;
+	}
+	return c;
+    }
+    /** This method efficently appends one node array onto the end of another.
+     * @param a The initial <strong>Node[]</strong>.
+     * @param b The <strong>Node[]</strong> to append.
+     * @return A <strong>Node[]</strong> containing a followed by b.
+     */
+    static public Node[] add(Node a[], Node b[]) {
+	Node[] c = null;
+	if(a != null && b != null) {
+	    c = new Node[a.length + b.length];
+	    System.arraycopy(a, 0, c, 0, a.length);
+	    System.arraycopy(b, 0, c, a.length, b.length);
+	}
+	else if(a == null && b != null) {
+	    c = b;
+	}
+	else if(a != null && b == null) {
+	    c = a;
+	}
+	return c;
+    }
+	 
+    /** This method efficiently appends a new string onto the end of a string array.
+     * @param a The initial <strong>String[]</strong>.
+     * @param b The new <strong>String</strong>.
+     * @return A <strong>String[]</strong> containing a followed by b.
+     */
+    static public String[] add(String a[], String b) {
+	String[] c = null;
+	if(a != null && b != null) {
+	    c = new String[a.length + 1];
+	    System.arraycopy(a, 0, c, 0, a.length);
+	    c[c.length - 1] = b;
+	}
+	else if(a == null && b != null) {
+	    c = new String[1];
+	    c[0] = b;
+	}
+	else if(a != null && b == null) {
+	    c = a;
+	}
+	return c;
+    }
+    /** This method efficently appends one string array onto the end of another.
+     * @param a The initial <strong>String[]</strong>.
+     * @param b The <strong>String[]</strong> to append.
+     * @return A <strong>String[]</strong> containing a followed by b.
+     */
+    static public String[] add(String a[], String b[]) {
+	String[] c = null;
+	if(a != null && b != null) {
+	    c = new String[a.length + b.length];
+	    System.arraycopy(a, 0, c, 0, a.length);
+	    System.arraycopy(b, 0, c, a.length, b.length);
+	}
+	else if(a == null && b != null) {
+	    c = b;
+	}
+	else if(a != null && b == null) {
+	    c = a;
+	}
+	return c;
+    }
+    /** This method takes an array list and creates a string array.
+     * @param a An <strong>ArrayList</strong> containing hopefully <strong>String</strong> or else this will fail.
+     * @return A <strong>String[]</strong> or <i>null</i> if the array could not be created.
+     */
+    static public String[] arrayListToStringArray(ArrayList a) {
+	String array[] = new String[a.size()];
+	for(int i = 0; i < array.length; i++) {
+	    array[i] = (String)a.get(i);
+	}
+	return array;
+    }
+
+    static public File[] filter(File[] files, String pattern, boolean exclude) {
+	int write_ptr = 0;
+	///ystem.err.println("Filtering by '" + pattern + "', Exclude? " + exclude + " :");
+	for(int read_ptr = 0; read_ptr < files.length; read_ptr++) {
+	    File current = files[read_ptr];
+	    files[write_ptr] = current;
+	    ///ystem.err.print("Testing " + current.getName() + " -> ");
+	    // Determine whether we move the write pointer or not.
+	    if(current.getName().toLowerCase().matches(pattern)) {
+		if(!exclude) {
+		    ///ystem.err.println("Match, Exclude.");
+		    write_ptr++;  
+		}
+		else {
+		    ///ystem.err.println("Match, Include.");
+		}
+	    }
+	    else {
+		// You can't exclude folders with an inclusion filter!
+		if(exclude || current.isDirectory()) {
+		    ///ystem.err.println("Nonmatch, Exclude.");
+		    write_ptr++;
+		}
+		else {
+		    ///ystem.err.println("Nonmatch, Include.");
+		}
+	    }
+	}
+	File[] result = new File[write_ptr];
+	System.arraycopy(files, 0, result, 0, result.length);
+	pattern = null;
+	files = null;
+	return result;
+    }
+
+
+    /** Transforms an Object array into a single string of the form '[o1,o2,...,on]'.
+     * @param objects An <strong>Object[]</strong>. Note that the objects in this array must support toString() reasonably.
+     * @return A <strong>String</strong> representing the given array.
+     */
+    static public String objectArrayToString(Object objects[]) {
+	StringBuffer result = new StringBuffer("[");
+	for(int i = 0; i < objects.length; i++) {
+	    result.append(objects[i].toString());
+	    if(i < objects.length - 1) {
+		result.append(",");
+	    }
+	}
+	result.append("]");
+	return result.toString();
+    }
+
+
+    /** Sorts an array of files, putting non-files first. Case insensitive.
+     * @param files_to_sort The File[] to be sorted.
+     */
+    static public void sort(File[] files_to_sort)
+    {
+	// Return if there is nothing to sort
+	if (files_to_sort == null || files_to_sort.length <= 1) {
+	    return;
+	}
+
+	FileSystemView fileSystemView = FileSystemView.getFileSystemView();
+	FileComparator comparator = new FileComparator();
+
+	// Separate the file system roots and directories from the files, and sort separately
+	ArrayList files_list = new ArrayList((files_to_sort.length * 3) / 4);
+	ArrayList non_files_list = new ArrayList((files_to_sort.length) / 4);
+	for (int i = 0; i < files_to_sort.length; i++) {
+	    File file = files_to_sort[i];
+
+	    // File is a system root
+	    if (fileSystemView.isFileSystemRoot(file)) {
+		non_files_list.add(file);
+	    }
+	    // File is a directory
+	    else if (file.isDirectory()) {
+		non_files_list.add(file);
+	    }
+	    // File is an actual file
+	    else {
+		files_list.add(file);
+	    }
+	}
+
+	// Sort files
+	Object[] files = files_list.toArray();
+	Arrays.sort(files, comparator);
+
+	// Sort non-files
+	Object[] non_files = non_files_list.toArray();
+	Arrays.sort(non_files, comparator);
+
+	// Merge the results, putting non-files before files
+	int j = 0;
+	for (int i = 0; i < non_files.length; i++) {
+	    files_to_sort[j++] = (File) non_files[i];
+	}
+	for (int i = 0; i < files.length; i++) {
+	    files_to_sort[j++] = (File) files[i];
+	}
+    }
+
+
+    /** Comparator used to order files. */
+    static private class FileComparator
+	implements Comparator {
+
+	/** Compare two files in terms of ordering of their paths.
+	 * @param o1 The <strong>Object</strong> that represents the first file.
+	 * @param o2 The other <strong>Object</strong>, also a file.
+	 * @return An <i>int</i> which is <1, 0 or >1 if o1 is <o2, =o2 or >o2 respectively.
+	 */
+	public int compare(Object o1, Object o2)
+	{
+	    File f1 = (File) o1;
+	    File f2 = (File) o2;
+
+	    return f1.getName().compareToIgnoreCase(f2.getName());
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/CheckList.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/CheckList.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/CheckList.java	(revision 31635)
@@ -0,0 +1,246 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.border.*;
+
+
+/** This class provides a visual component that has the form of a list, as provided by JList but uses JCheckBox for data selection. Thus several elements can be 'ticked' in the list, and this selection returned using the method getSelected().<BR>Parts of this code modified from Trevor Harmon's posting on www.dejanews.com.
+ * @author John Thompson
+ * @version 2.3
+ */ 
+public class CheckList
+    extends JList
+{
+    /** The border used when a list row is not in focus. */
+    static private Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);
+
+    private boolean show_selected_row = true;
+
+
+    /** Constructor. */
+    public CheckList(boolean show_selected_row)
+    {
+	super();
+	this.show_selected_row = show_selected_row;
+
+	setCellRenderer(new CheckListCellRenderer());
+	setModel(new DefaultListModel());
+	setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+	addMouseListener(new CheckListMouseListener());
+    }
+
+
+    public void addEntry(CheckListEntry entry)
+    {
+	DefaultListModel model = (DefaultListModel) getModel();
+
+	// Add the entry in alpabetical order
+	String name = entry.toString();
+	for (int i = 0; i < model.size(); i++) {
+	    Object sibling = model.getElementAt(i);
+	    if (name.compareTo(sibling.toString()) <= 0) {
+		model.add(i, entry);
+		return;
+	    }
+	}
+
+	model.addElement(entry);
+    }
+
+
+    public void clearTicked()
+    {
+	DefaultListModel model = (DefaultListModel) getModel();
+	for (int i = 0; i < model.size(); i++) {
+	    ((CheckListEntry) model.get(i)).setSelected(false);
+	}
+	updateUI();
+    }
+
+    /** Retrieve all the entries from the list
+     */
+    public ArrayList getAll()
+    {
+	ArrayList result = new ArrayList();
+	DefaultListModel model = (DefaultListModel) getModel();
+	for (int i = 0; i < model.size(); i++) {
+	    CheckListEntry entry = (CheckListEntry) model.get(i);
+	    result.add(entry.getObject());
+	}
+	return result;
+    }
+	
+
+    /** Retrieve the currently ticked entries from this list.
+     * @return An <strong>ArrayList</strong> containing only those entries from the initial list that are checked.
+     * @see org.greenstone.gatherer.checklist.Entry
+     */
+    public ArrayList getTicked()
+    {
+	ArrayList result = new ArrayList();
+	DefaultListModel model = (DefaultListModel) getModel();
+	for (int i = 0; i < model.size(); i++) {
+	    CheckListEntry entry = (CheckListEntry) model.get(i);
+	    if (entry.isSelected()) {
+		result.add(entry.getObject());
+	    }
+	}
+	return result;
+    }
+
+
+    /** This is different from isSelectionEmpty! */
+    public boolean isNothingTicked()
+    {
+	DefaultListModel model = (DefaultListModel) getModel();
+	for (int i = 0; i < model.size(); i++) {
+	    if (((CheckListEntry) model.get(i)).isSelected()) {
+		return false;
+	    }
+	}
+
+	return true;
+    }
+
+
+    public void setListData(ArrayList list_data)
+    {
+ 	// Create a new model.
+	setModel(new DefaultListModel());
+
+	// Add the items from the list to the model
+ 	for (int i = 0; i < list_data.size(); i++) {
+ 	    Object list_object = list_data.get(i);
+ 	    if (list_object instanceof CheckListEntry) {
+ 		addEntry((CheckListEntry) list_object);
+ 	    }
+	    else {
+		addEntry(new CheckListEntry(list_object));
+	    }
+ 	}
+    }
+
+
+    public void setTickedObjects(Object[] objects)
+    {
+	if (objects == null) {
+	    return;
+	}
+
+	DefaultListModel model = (DefaultListModel) getModel();
+	for (int i = 0; i < model.size(); i++) {
+	    CheckListEntry entry = (CheckListEntry) model.get(i);
+	    for (int j = 0; j < objects.length; j++) {
+		if (entry.getObject().equals(objects[j])) {
+		    entry.setSelected(true);
+		}
+	    }
+	}
+	updateUI();
+    }
+
+    public void setAllTicked() {
+	DefaultListModel model = (DefaultListModel) getModel();
+	for (int i = 0; i < model.size(); i++) {
+	    CheckListEntry entry = (CheckListEntry) model.get(i);
+	    entry.setSelected(true);
+	}
+	updateUI();
+    }
+
+    /** A custom list cell renderer for producing rows which contain clickable check boxes. */
+    protected class CheckListCellRenderer 
+	implements ListCellRenderer
+    {
+	/** Return a component that has been configured to display the specified value. That component's paint method is then called to "render" the cell. If it is necessary to compute the dimensions of a list because the list cells do not have a fixed size, this method is called to generate a component on which getPreferredSize  can be invoked.
+	 * @param list The </strong>JList</strong> we're painting.
+	 * @param value The value returned by list.getModel().getElementAt(index), as an <strong>Object</strong>.
+	 * @param index The cells index as an <i>int</i>.
+	 * @param is_selected <i>true</i> if the specified cell was selected, <i>false</i> otherwise.
+	 * @param cell_has_focus <i>true</i> if and only if the specified cell has the focus.
+	 * @return A <strong>Component</strong> whose paint() method will render the specified value.
+	 */
+	public Component getListCellRendererComponent(JList list, Object value, int index, boolean is_selected, boolean cell_has_focus) {
+	    JCheckBox checkbox = (JCheckBox) value;
+	    if (show_selected_row) {
+		checkbox.setBackground(is_selected ? list.getSelectionBackground() : list.getBackground());
+		checkbox.setForeground(is_selected ? list.getSelectionForeground() : list.getForeground());
+		checkbox.setBorderPainted(true);
+	    }
+	    else {
+		checkbox.setBackground(list.getBackground());
+		checkbox.setForeground(list.getForeground());
+		checkbox.setBorderPainted(false);
+	    }
+	    checkbox.setEnabled(list.isEnabled());
+	    checkbox.setFont(list.getFont());
+	    checkbox.setFocusPainted(false);
+	    checkbox.setBorder((is_selected) ? UIManager.getBorder("List.focusCellHighlightBorder") : noFocusBorder);
+	    return checkbox;
+	}
+    }
+
+
+    /** Listens for clicks apon the checks within the list, and updates as necessary. */
+    private class CheckListMouseListener 
+	extends MouseAdapter
+    {
+	/** Called whenever the mouse is clicked over our list. We find the nearest checkbox and change its state.
+	 * @param e A <strong>MouseEvent</strong> containing everything you ever wanted to know about the mouse event but were afraid to ask.
+	 */
+	public void mousePressed(MouseEvent e)
+	{
+	    if (!isEnabled()) return;
+	    JList list = (JList) e.getSource();
+	    int index = list.locationToIndex(e.getPoint());
+	    CheckListEntry checkbox = (CheckListEntry) list.getModel().getElementAt(index);
+	    if (!checkbox.isFixed()) {
+		checkbox.setSelected(!checkbox.isSelected());
+	    }
+	    checkbox.grabFocus();
+
+	    // We need to cause a ListSelectionEvent -- this is ugly but I can't find a better way quickly
+	    list.removeSelectionInterval(0, 0);
+	    list.setSelectionInterval(index, index);
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/CheckListEntry.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/CheckListEntry.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/CheckListEntry.java	(revision 31635)
@@ -0,0 +1,81 @@
+package org.greenstone.gatherer.util;
+
+import javax.swing.JCheckBox;
+import org.greenstone.gatherer.Dictionary;
+
+/** A CheckListEntry encapsulates a single row of the list, both its check box and the object the row represents. */
+public class CheckListEntry
+    extends JCheckBox 
+    implements Comparable {
+
+    /** Is this checkboxes state fixed. */
+    private boolean fixed = false;
+    /** The original object this row is based on, and whose toString() is used to generate the check box label. */
+    private Object object = null;
+    /** A sundry string for storing original values (rather than strings retrieved from dictionary). */
+    private String property;
+    /** Constructor. */
+    public CheckListEntry(Object object) {
+	super(object.toString());
+	this.object = object;
+	this.setComponentOrientation(Dictionary.getOrientation());
+    }
+
+    /** Constructor. */
+    public CheckListEntry(String text, boolean is_selected) {
+	super(text);
+	this.setComponentOrientation(Dictionary.getOrientation());
+	setSelected(is_selected);
+    }
+
+    /** Test this CheckListEntry against an Object for ordering. 
+     * @param obj the other Object.
+     * @return <0, 0 or >0 if this CheckListEntry is before, equal to or after the given object.
+     */
+    public int compareTo(Object obj) {
+	return toString().toLowerCase().compareTo(obj.toString().toLowerCase());
+    }
+    /** Test this CheckListEntry against an Object for equality. 
+     * @param obj the other Object.
+     * @return true if the two are equal, false otherwise.
+     */
+    public boolean equals(Object obj) {
+	if (obj == null) {
+	    return false;
+	}
+	return toString().equalsIgnoreCase(obj.toString());
+    }
+
+    /** Retrieve the object associated with this entry.
+     * @return An <strong>Object</strong>.
+     */
+    public Object getObject() {
+	if(object == null) {
+	    return getText();
+	}
+	return object;
+    }
+
+    public String getProperty() {
+	return property;
+    }
+
+    public boolean isFixed() {
+	return fixed;
+    }
+
+    public void setFixed(boolean fixed) {
+	this.fixed = fixed;
+    }
+
+    public void setProperty(String property) {
+	this.property = property;
+    }
+
+    public String toString() {
+	if(object == null) {
+	    return getText();
+	}
+	return object.toString();
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/Codec.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/Codec.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/Codec.java	(revision 31635)
@@ -0,0 +1,391 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+
+import java.util.*;
+
+/** Provides a standard, extensible way to convert from one format of string to another (given that each format has differing requirements regarding legal characters and escaped characters)
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3d
+ */
+public class Codec {
+    
+    static final public String DECODE_PATH = "DECODE_PATH";
+    static final public String DECODE_SQUARE_BRACKETS = "DECODE_SQUARE_BRACKETS";
+    static final public String DOM_TO_GREENSTONE = "DOM_TO_GREENSTONE";
+    static final public String DOM_TO_TEXT = "DOM_TO_TEXT";
+    static final public String ENCODE_PATH = "ENCODE_PATH";
+    static final public String ENCODE_SQUARE_BRACKETS = "ENCODE_SQUARE_BRACKETS";
+    static final public String ESCAPEDHTML_TO_UNESCAPED = "ESCAPEDHTML_TO_UNESCAPED";
+    static final public String GREENSTONE_TO_DOM = "GREENSTONE_TO_DOM";
+    static final public String GREENSTONE_TO_TEXT = "GREENSTONE_TO_TEXT";
+    static final public String TEXT_TO_DOM = "TEXT_TO_DOM";
+    static final public String TEXT_TO_GREENSTONE = "TEXT_TO_GREENSTONE";
+    static final public String TEXT_TO_REGEXP = "TEXT_TO_REGEXP";
+    static final public String TEXT_TO_SHELL_UNIX = "TEXT_TO_SHELL_UNIX";
+    static final public String TEXT_TO_SHELL_WINDOWS = "TEXT_TO_SHELL_WINDOWS";
+    
+    static final private int MAX_CACHE_SIZE = 100;
+    
+    static private HashMap TRANSFORMS;
+    static private HashMap3D CACHE;
+    
+    /** Static function called to construct TRANSFORMS mappings */
+    static {
+	TRANSFORMS = new HashMap();
+
+	String[] decode_path = {
+	    "\\|",    "\\\\",
+	    "&#124;", "\\|"
+	};
+	TRANSFORMS.put(DECODE_PATH, decode_path);
+	decode_path = null;
+
+	// Transform text into text, but without [ and ]
+	String[] decode_square_brackets = {
+	    "&#091;", "\\[",
+	    "&#093;", "\\]" 
+	};
+	TRANSFORMS.put(DECODE_SQUARE_BRACKETS, decode_square_brackets);
+	decode_square_brackets = null;
+
+	// Translate DOM encoded text into Greenstone encoding
+	String[] dom_to_greenstone = {
+	    "&apos;", "\\\\\'",
+	    "&gt;", ">",
+	    "&lt;", "<",
+	    "&quot;", "\\\\\"",
+	    "&amp;", "&"
+	};
+	//  removed "\n", "\\\\n", - config files are allowed new lines
+	// added "\\|", "\\\\"
+
+	TRANSFORMS.put(DOM_TO_GREENSTONE, dom_to_greenstone);
+	dom_to_greenstone = null;
+
+	// Transform DOM encoded text into plain text
+	String[] dom_to_text = {
+	   "&amp;#091;", "\\[",
+	   "&amp;#093;", "\\]",
+	   "&apos;", "\'",
+	   "&gt;", ">",
+	   "&lt;", "<",
+	   "&quot;", "\"",
+	   "&amp;", "&"
+	};
+	TRANSFORMS.put(DOM_TO_TEXT, dom_to_text);
+	dom_to_text = null;
+
+	// Transform text into a regular expression that will match it
+	String[] text_to_regexp = {
+	    "\\\\", "\\\\\\\\",
+	    "\\(", "\\\\(",
+	    "\\)", "\\\\)",
+	    "\\[", "\\\\[",
+	    "\\]", "\\\\]",
+	    "\\{", "\\\\{",
+	    "\\}", "\\\\}",
+	    "\\.", "\\\\."
+	};
+	TRANSFORMS.put(TEXT_TO_REGEXP, text_to_regexp);
+	text_to_regexp = null;
+
+	String[] encode_path = {
+	    "\\|",  "&#124;",
+	    "\\\\", "\\|"
+	};
+	TRANSFORMS.put(ENCODE_PATH, encode_path);
+	encode_path = null;
+
+	// Transform text into text, but without [ and ]
+	String[] encode_square_brackets = {
+	    "\\[", "&#091;",
+	    "\\]", "&#093;"
+	};
+	TRANSFORMS.put(ENCODE_SQUARE_BRACKETS, encode_square_brackets);
+	encode_square_brackets = null;
+
+	// Transform Greenstone encoded text to DOM encoding
+	String[] greenstone_to_dom = {
+	    "&", "&amp;",
+	    "<", "&lt;",
+	    ">", "&gt;",
+	    "\\\\\"", "&quot;",
+	    "\\\\\'", "&apos;",
+	    "\"", "&quot;",
+	    "\'", "&apos;"
+	};
+	// removed"\\\\n", "\n", added "\\\\", "\\|"
+
+	TRANSFORMS.put(GREENSTONE_TO_DOM, greenstone_to_dom);
+	greenstone_to_dom = null;
+
+	// Transform Greenstone encoded text to plain text
+	String[] greenstone_to_text = {
+	    "\\\\\"", "\"",
+	    "\\\\\'", "\'",
+	    "&quot;", "\"",
+	    "&apos;", "\'",
+	    "&#091;", "\\[",
+	    "&#093;", "\\]"
+	};
+	// removed    "\\\\n", "\n", "\\|", "\\\\"
+
+	TRANSFORMS.put(GREENSTONE_TO_TEXT, greenstone_to_text);
+	greenstone_to_text = null;
+
+	// Transform plain html text into something that can be placed in a DOM
+	String[] text_to_dom = {
+	    "&", "&amp;",
+	    "<", "&lt;",
+	    ">", "&gt;",
+	    "\"", "&quot;",
+	    "\'", "&apos;"
+	};
+	TRANSFORMS.put(TEXT_TO_DOM, text_to_dom);
+	text_to_dom = null;
+
+	// Unescape html (or xml) text
+	String[] escapedhtml_to_unescaped = {
+	    "&amp;", "&",
+	    "&lt;", "<", 
+	    "&gt;", ">",
+	    "&quot;", "\""//,
+	    //"&apos;", "\'"
+	};
+	TRANSFORMS.put(ESCAPEDHTML_TO_UNESCAPED, escapedhtml_to_unescaped);
+	escapedhtml_to_unescaped = null;
+
+	// Transform plain html text into greenstone encoding
+	String[] text_to_greenstone = {
+
+	    "\\[", "&#091;",
+	    "\\]", "&#093;",
+	    "\"", "&quot;",
+	    "\n", "\\\\n"
+	};
+	// 	    "\'", "&apos;",
+	// removed "\\\\", "\\|",
+	TRANSFORMS.put(TEXT_TO_GREENSTONE, text_to_greenstone);
+	text_to_greenstone = null;
+
+	// Transform plain html text into something that can be placed in a shell command
+	String[] text_to_shell_unix = {
+	    "\"", "\\\\\"",
+	    "\'", "\\\\\'",
+	    "\n", "\\\\n"
+	};
+	TRANSFORMS.put(TEXT_TO_SHELL_UNIX, text_to_shell_unix);
+	text_to_shell_unix = null;
+
+	// Transform plain html text into something that can be placed in a shell command. Windows requires twice as many escaped for speech marks to be passed to underlying processes
+	String[] text_to_shell_windows = {
+	    "\"", "\\\\\\\\\\\\\"",
+	    "\'", "\\\\\'",
+	    "\n", "\\\\n"
+	};
+	TRANSFORMS.put(TEXT_TO_SHELL_WINDOWS, text_to_shell_windows);
+	text_to_shell_windows = null;
+
+	CACHE = new HashMap3D();
+    }
+
+    static public String transform(String raw, String transform) {
+	if(raw == null) {
+	    return raw;
+	}
+	// System.err.println("Transforming by "+transform+":\n" + raw);
+	String processed = (String) CACHE.get(transform, raw);
+	if(processed == null) {
+	    processed = raw;
+	    String[] transforms = (String[]) TRANSFORMS.get(transform);
+	    if(transforms != null) {
+		for(int i = 0; i < transforms.length; i = i + 2) {
+		    String target = transforms[i];
+		    String result = transforms[i+1];
+		    processed = processed.replaceAll(target, result);
+		}
+	    }
+	    //DebugStream.println("\n*** Transform: " + transform + " ***");
+	    //DebugStream.println("*** Raw      : '" + raw + "'");
+	    //DebugStream.println("*** Processed: '" + processed + "'");
+	    // If cache is at maximum size, empty it and start again
+	    if(CACHE.size() == MAX_CACHE_SIZE) {
+		CACHE.clear();
+	    }
+	    CACHE.put(transform, raw, processed);
+	}
+	return processed;
+    }
+
+    /** Transform either of the accepted unicode escape sequences styles from in the string into single characters */
+    static final private char AND_CHAR = '&';
+    static final private char ESCAPE_CHAR = '\\';
+    static final private char HASH_CHAR = '#';
+    static final private char LOWER_U_CHAR = 'u';
+    static final private char UPPER_U_CHAR = 'U';
+    static final private char SEMICOLON_CHAR = ';';
+
+    static public String transformUnicode(String raw) {
+	StringBuffer processed = new StringBuffer();
+	int index = 0;
+	int raw_length = raw.length();
+	while(index < raw_length) {
+	    char c0 = raw.charAt(index);
+	    switch(c0) {
+	    case AND_CHAR:
+		if(index + 1 < raw_length) {
+		    // First the HTML &#231; type
+		    char c1 = raw.charAt(index + 1);
+		    if(c1 == HASH_CHAR) {
+			StringBuffer number_str = new StringBuffer();
+			char c2;
+			int offset = 2;
+			while(index + offset < raw_length && (c2 = raw.charAt(index + offset)) != SEMICOLON_CHAR) {
+			    number_str.append(c2);
+			    offset++;
+			}
+			// We've either run out of characters or have parsed a number
+			if(index + offset < raw_length && raw.charAt(index + offset) == SEMICOLON_CHAR) {
+			    int number = Integer.parseInt(number_str.toString());
+			    processed.append((char)number);
+			    index = index + offset;
+			    number_str = null;
+			    break;
+			}
+			number_str = null;
+		    }
+		}
+		processed.append(c0);
+		break;
+	    case ESCAPE_CHAR:
+		// Now the \u00e7 type
+		if(index + 1 < raw_length) {
+		    char c3 = raw.charAt(index + 1);
+		    if((c3 == UPPER_U_CHAR || c3 == LOWER_U_CHAR) && index + 5 < raw_length) {
+			// We read four digits
+			String hex_str = raw.substring(index + 2, index + 6);
+			int number = Integer.parseInt(hex_str, 16);
+			hex_str = null;
+			processed.append((char)number);
+			index = index + 5;
+			break;
+		    }
+		}
+		processed.append(c0);
+		break;
+	    default:
+		processed.append(c0);
+	    }
+	    index++;
+	}
+	return processed.toString();
+    }
+
+    static public void main(String[] args) {
+	if(args.length < 2) {
+	    String processed;
+	    String raw;
+	    String transform;
+
+	    System.err.println("Running Test Suite");
+
+	    transform = "DOM_TO_GREENSTONE";
+	    System.err.println("Test " + transform);
+	    raw = "A &amp;lt;\nand a &lt;a href=&quot;here.html&quot;&gt;&lt;font size=&apos;2&apos;&gt;URL&lt;/font&gt;&lt;/a&gt;";
+	    System.err.println("Raw:       '" + raw + "'");
+	    processed = transform(raw, transform);
+	    System.err.println("Processed: '" + processed + "'");
+
+	    transform = "DOM_TO_TEXT";
+	    System.err.println("Test " + transform);
+	    raw = "A &amp;lt;\nand a &lt;a href=&quot;here.html&quot;&gt;&lt;font size=&apos;2&apos;&gt;URL&lt;/font&gt;&lt;/a&gt;";
+	    System.err.println("Raw:       '" + raw + "'");
+	    processed = transform(raw, transform);
+	    System.err.println("Processed: '" + processed + "'");
+
+	    transform = "GREENSTONE_TO_DOM";
+	    System.err.println("Test " + transform);
+	    raw = "A &lt;\\nand a <a href=\\\"here.html\\\"><font size=\\\'2\\\'URL</font></a>";
+	    System.err.println("Raw:       '" + raw + "'");
+	    processed = transform(raw, transform);
+	    System.err.println("Processed: '" + processed + "'");
+
+	    transform = "GREENSTONE_TO_TEXT";
+	    System.err.println("Test " + transform);
+	    raw = "These \\[ \\] should be escaped, and so should \\\\ that. These &quot; &apos; \\n are encoded.";
+	    System.err.println("Raw:       '" + raw + "'");
+	    processed = transform(raw, transform);
+	    System.err.println("Processed: '" + processed + "'");
+
+	    transform = "TEXT_TO_DOM";
+	    System.err.println("Test " + transform);
+	    raw = "A &lt;\nand a <a href=\"here.html\"><font size='2'>URL</font></a>";
+	    System.err.println("Raw:       '" + raw + "'");
+	    processed = transform(raw, transform);
+	    System.err.println("Processed: '" + processed + "'");
+
+	    transform = "TEXT_TO_GREENSTONE";
+	    System.err.println("Test " + transform);
+	    raw = "These [ ] should be escaped, and so should \\ that. These \" \' \n are encoded.";
+	    System.err.println("Raw:       '" + raw + "'");
+	    processed = transform(raw, transform);
+	    System.err.println("Processed: '" + processed + "'");
+
+	    transform = "TEXT_TO_SHELL";
+	    System.err.println("Test " + transform);
+	    if(Utility.isWindows()) {
+		System.err.println("[Windows Version]");
+		transform = "TEXT_TO_SHELL_WINDOWS";
+	    }
+	    else {
+		System.err.println("[Unix Version]");
+		transform = "TEXT_TO_SHELL_UNIX";
+	    }
+	    raw = "A &lt;\nand a <a href=\"here.html\"><font size='2'>URL</font></a>";
+	    System.err.println("Raw:       '" + raw + "'");
+	    processed = transform(raw, transform);
+	    System.err.println("Processed: '" + processed + "'");
+
+	    System.err.println("***** UNICODE TEST *****");
+	    System.err.println("\\u0030 => " + transformUnicode("\\u0030"));
+	    System.err.println("\\u0041 => " + transformUnicode("\\u0041"));
+	    System.err.println("\\u007a => " + transformUnicode("\\u007a"));
+	    System.err.println("\\u00e7 => " + transformUnicode("\\u00e7"));
+	    System.err.println("&#48;   => " + transformUnicode("&#48;"));
+	    System.err.println("&#65;   => " + transformUnicode("&#65;"));
+	    System.err.println("&#122;  => " + transformUnicode("&#122;"));
+	    System.err.println("&#231;  => " + transformUnicode("&#231;"));
+	}
+	else {
+	    System.err.println("Raw:       '" + args[0] + "'");
+	    System.err.println("Transform: " + args[1]);
+	    String processed = transform(args[0], args[1]);
+	    System.err.println("Processed: '" + processed + "'");
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DOMTree.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DOMTree.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DOMTree.java	(revision 31635)
@@ -0,0 +1,149 @@
+package org.greenstone.gatherer.util;
+
+import java.util.*;
+import javax.swing.*;
+import javax.swing.tree.*;
+import org.w3c.dom.*;
+
+public class DOMTree
+    extends JTree {
+
+    public DOMTree(Document document) {
+	super();
+	setDocument(document);
+    }
+
+    public void setDocument(Document document) {
+	setModel(new DefaultTreeModel(new DOMTreeNode(document.getDocumentElement(), null)));
+    }
+
+    private class DOMTreeNode 
+	implements TreeNode {
+
+	private Node node = null;
+	private NodeList children = null;
+	private String text = null;
+	private TreeNode parent = null;
+	private TreeNode self = null;
+
+	DOMTreeNode(Node node, TreeNode parent) {
+	    this.node = node;
+	    this.parent = parent;
+	    this.self = this;
+	}
+	
+	/** Returns the children of the receiver as an Enumeration. */
+	public Enumeration children() {
+	    if(children == null) {
+		children = node.getChildNodes();
+	    }
+	    return new NodeListEnumeration();
+	}
+          
+	/** Returns true if the receiver allows children. */
+	public boolean getAllowsChildren() {
+	    return true;
+	}
+          
+	/** Returns the child TreeNode at index childIndex. */
+	public TreeNode getChildAt(int childIndex) {
+	    if(children == null) {
+		children = node.getChildNodes();
+	    }
+	    return new DOMTreeNode(children.item(childIndex), this);
+	}
+          
+	/** Returns the number of children TreeNodes the receiver contains. */
+	public int getChildCount() {
+	    if(children == null) {
+		children = node.getChildNodes();
+	    }
+	    return children.getLength();
+	}
+          
+	/** Returns the index of node in the receivers children. */
+	public int getIndex(TreeNode find_node) {
+	    if(children == null) {
+		children = node.getChildNodes();
+	    }
+	    Node target_node = ((DOMTreeNode)find_node).getNode();
+	    int children_count = children.getLength();
+	    for(int i = 0; i < children_count; i++) {
+		Node test_node = children.item(i);
+		if(target_node == test_node) {
+		    test_node = null;
+		    target_node = null;
+		    return i;
+		}
+		test_node = null;
+	    }
+	    target_node = null;
+	    return -1;
+	}
+	    
+	public Node getNode() {
+	    return node;
+	}
+
+	/** Returns the parent TreeNode of the receiver. */
+	public TreeNode getParent() {
+	    return parent;
+	}
+	    
+	/** Returns true if the receiver is a leaf. */
+	public boolean isLeaf() {
+	    return !node.hasChildNodes();
+	}
+
+	public String toString() {
+	    if(text == null) {
+		StringBuffer temp = new StringBuffer(node.getNodeName());
+		String value = node.getNodeValue();
+		if(value != null && value.length() != 0) {
+		    temp.append("=\"");
+		    temp.append(value);
+		    temp.append("\"");
+		}
+		value = null;
+		if(node instanceof Element) {
+		    Element element = (Element) node;
+		    temp.append(": ");
+		    NamedNodeMap attributes = element.getAttributes();
+		    int attribute_count = attributes.getLength();
+		    for(int i = 0; i < attribute_count; i++) {
+			Node attribute = attributes.item(i);
+			temp.append(attribute.getNodeName());
+			temp.append("=\"");
+			temp.append(attribute.getNodeValue());
+			if(i < attribute_count - 1) {
+			    temp.append("\" ");
+			}
+			else {
+			    temp.append("\"");
+			}
+		    }
+		    attributes = null;
+		    element = null;
+		}
+		text = temp.toString();
+		temp = null;
+	    }
+	    return text;
+	}
+
+	private class NodeListEnumeration
+	    implements Enumeration {
+	    private int index = 0;
+	    /** Tests if this enumeration contains more elements. */
+	    public boolean hasMoreElements() {
+		return index < children.getLength();
+	    }
+	    /** Returns the next element of this enumeration if this enumeration object has at least one more element to provide. */
+	    public Object nextElement() {
+		int old_index = index;
+		index = index + 1;
+		return new DOMTreeNode(children.item(old_index), self);
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DragComponent.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DragComponent.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DragComponent.java	(revision 31635)
@@ -0,0 +1,65 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+/**
+ * Title:        Gatherer<br>
+ * Description:  The Gatherer: a tool for gathering and enriching a digital collection.<br>
+ * Copyright:    Copyright (c) 2001<br>
+ * Company:      The University of Waikato<br>
+ * Written:        /  /01<br>
+ * Revised:      21/06/02 - Moved into correct package and commented.<br>
+ * @author John Thompson, 9826509
+ * @version 2.1
+ */
+import java.awt.event.FocusListener;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.file.FileSystemModel;
+
+/** Any component that wants to act a GTree like drag'n'drop component must implement this. */
+public interface DragComponent {
+    public void addFocusListener(FocusListener listener);
+    /** In order for the appearance to be consistant, given we may be in the situation where the pointer has left our focus but the ghost remains, this method allows other members of the Group to tell this component to repair the 'spoilt' region left by its ghost. */
+    public void clearGhost();
+    /** Used to notify this component that it has gained focus by some method other that mouse focus. */
+    public void gainFocus();	 
+    /** Retrieve the model associated with this component. */
+    public FileSystemModel getTreeModel();
+    /** This method is used to inform this component when it loses focus by means other than a drag mouse event, and should indicate this somehow. */
+    public void loseFocus();
+    /** Set the components group. */
+    public void setGroup(DragGroup group);
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DragGroup.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DragGroup.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DragGroup.java	(revision 31635)
@@ -0,0 +1,136 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+
+import java.awt.Point;
+import java.awt.event.*;
+import java.awt.image.BufferedImage;
+import java.util.Vector;
+import javax.swing.JComponent;
+import javax.swing.tree.TreePath;
+import org.greenstone.gatherer.gui.tree.DragTree;
+
+
+/** This class acts as a linker between all the various drag and drop enabled DragComponent. It provides methods for ensuring only one component is the drop target, showing extra user feedback, and maintains a single point of focus. Moreover it provides a storage space for necessary shared variables such as mouse_offset of initial drag etc. */
+public class DragGroup 
+    implements FocusListener {
+    /** The image used for the ghost icon when dragging. */
+    public BufferedImage image_ghost = null;
+    /** The initial offset of the mouse from the selection within a DragComponent. Then as you drag the mouse, the ghost appears in a visually consistant place, and remains at the same offset throughout the dragging process. */
+    public Point mouse_offset = null;
+    /** The component currently in charge of drawing the ghost icon. Note that this isn't necessarily the only component that needs to repaint 'spoilt' screen real-estate as a side effect of the ghost. */
+    private DragComponent ghost_owner = null;
+    /** The component where the drag began, so that extra information necessary for the drag can be garnered by the drop target, or so that focus can return to the source component if the drop is rejected. */
+    private DragTree drag_source = null;
+    /** The selected nodes, as determined when the drag begun. */
+    private TreePath[] selection = null;
+    /** A list of DragComponents registered as being part of this group. */
+    private Vector registered = new Vector();
+
+    /** Register a DragComponent as begin part of this group.
+     * @param component The DragComponent to add.
+     */
+    public void add(DragComponent component) {
+	if(!registered.contains(component)) {
+	    registered.add(component);
+	    component.addFocusListener(this);
+	    component.setGroup(this);
+	}
+    }
+
+    /** Invoked when a component gains the keyboard focus. */
+    public void focusGained(FocusEvent event) {
+	((DragComponent)event.getComponent()).gainFocus();
+    }
+          
+    /** Invoked when a component loses the keyboard focus. */
+    public void focusLost(FocusEvent event) {
+	((DragComponent)event.getComponent()).loseFocus();
+    }          
+    /** Determines the current 'active' component, ie that component with focus.
+     * @return The DragComponent which is currently responsible for drawing the ghost, and thus is active.
+     */
+    public DragComponent getActive() {
+	return ghost_owner;
+    }
+    /** Retrieve the nodes selected at the beginning of this drag operation.
+     * @return A TreePath[].
+     */
+    public TreePath[] getSelection() {
+	return selection;
+    }
+    /** Retrieve the component which served as the source of this drag action.
+     * @return The DragComponent in question.
+     */
+    public DragTree getSource() {
+	return drag_source;
+    }
+    /** When called this method asserts that one of the registered GComponents just became the proud owner of focus, and is henceforth responsible for drawing the ghost, and acting like a component thats in focus.
+     * @param new_owner The DragComponent that has gained focus during a drag action.
+     */
+    public void grabFocus(DragComponent new_owner) {
+	// Tell the previous owner of ghost to clear itself.
+	if(ghost_owner != null) {
+	    ghost_owner.clearGhost();
+	}
+	// Assign new owner
+	ghost_owner = new_owner;
+	// Then tell all other GComponents to indicate they aren't in focus in whatever way they do that (ie selected items of a GTree go gray). Of course the new owners told it is in focus.
+	for(int i = 0; i < registered.size(); i++) {
+	    DragComponent comp = (DragComponent)registered.get(i);
+	    if(comp == new_owner) {
+		comp.gainFocus();
+	    }
+	    else {
+		comp.loseFocus();
+	    }
+	}
+    }
+    /** Sets the value of selected_nodes.
+     * @param selection The new value for selected_nodes as a TreePath[].
+     */
+    public void setSelection(TreePath[] selection) {
+	this.selection = selection;
+    }
+    /** Sets the value of drag_source.
+     * @param drag_source The new value for drag_source as a DragTree.
+     */
+    public void setSource(DragTree drag_source) {
+	this.drag_source = drag_source;
+	///ystem.err.println("The drag source is now " + drag_source);
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DragTreeSelectionModel.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DragTreeSelectionModel.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/DragTreeSelectionModel.java	(revision 31635)
@@ -0,0 +1,278 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+
+import java.awt.Point;
+import java.awt.event.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.gui.tree.DragTree;
+
+public class DragTreeSelectionModel
+    extends DefaultTreeSelectionModel
+    implements MouseListener {
+
+    private boolean immediate = false;
+    private boolean mixed_selection = false;
+
+    private int type               = -1;
+
+    private TreePath temp_path     = null;
+    private TreePath temp_paths[]  = null;
+
+    static private final int NONE  = -1;
+    static private final int ADD   =  0;
+    static private final int SET   =  1;
+
+    /** Constructor.
+     * @param tree the DragTree which will use this selection model.
+     * @param mixed_selection true if this selection model allows selections to contain files and folders, false otherwise.
+     */
+    public DragTreeSelectionModel(DragTree tree, boolean mixed_selection) {
+	super();
+	this.mixed_selection = mixed_selection;
+	setSelectionMode(DISCONTIGUOUS_TREE_SELECTION);
+	tree.addMouseListener(this);
+    }
+
+    public void addSelectionPath(TreePath path) {
+	if(!immediate) {
+	    this.temp_path = path;
+	    this.type = this.ADD;
+	}
+	else if(isAppropriate(path)) {
+	    temp_path = null;
+	    super.addSelectionPath(path);
+	}
+    }
+
+    public void addSelectionPaths(TreePath paths[]) {
+	if(!immediate) {
+	    this.temp_paths = paths;
+	    this.type = this.ADD;
+	}
+	else if(isAppropriate(paths, true)) {
+	    temp_paths = null;
+	    super.setSelectionPaths(paths);
+	}
+    }
+
+    public String getDetails() {
+	String suffix = null;
+	int file_count = 0;
+	int folder_count = 0;
+	for(int i = 0; selection != null && i < selection.length; i++) {
+	    TreeNode node = (TreeNode) selection[i].getLastPathComponent();
+	    if(node.isLeaf()) {
+		file_count++;
+	    }
+	    else {
+		folder_count++;
+	    }
+	}
+
+	// Update the metaaudit_suffix
+	String args[] = null;
+	if(file_count > 0 && folder_count > 0) {
+	    if(file_count > 1 && folder_count > 1) {
+		args = new String[2];
+		args[0] = String.valueOf(file_count);
+		args[1] = String.valueOf(folder_count);
+		suffix = Dictionary.get("FileActions.Selected", args);
+	    }
+	    else if(file_count > 1) {
+		args = new String[1];
+		args[0] = String.valueOf(file_count);
+		suffix = Dictionary.get("FileActions.Files_And_Directory_Selected", args);
+	    }
+	    else if(folder_count > 1) {
+		args = new String[1];
+		args[0] = String.valueOf(folder_count);
+		suffix = Dictionary.get("FileActions.File_And_Directories_Selected", args);
+	    }
+	    else {
+		suffix = Dictionary.get("FileActions.File_And_Directory_Selected");
+	    }
+	}
+	else if(file_count > 0) {
+	    if(file_count > 1) {
+		args = new String[1];
+		args[0] = String.valueOf(file_count);
+		suffix = Dictionary.get("FileActions.Files_Selected", args);
+	    }
+	    else if(file_count == 1) {
+		suffix = Dictionary.get("FileActions.File_Selected");
+	    }
+	}
+	else if(folder_count > 0) {
+	    if(folder_count > 1) {
+		args = new String[1];
+		args[0] = String.valueOf(folder_count);
+		suffix = Dictionary.get("FileActions.Directories_Selected", args);
+	    }
+	    else {
+		suffix = Dictionary.get("FileActions.Directory_Selected");
+	    }
+	}
+	args = null;
+
+	return suffix;
+    }
+
+    /** Any implementation of MouseListener must include this method so
+     * we can be informed when the mouse is clicked.
+     * @param event A MouseEvent containing all the information about
+     * this mouse click.
+     */
+    public void mouseClicked(MouseEvent event) {
+    }
+
+    /** Any implementation of MouseListener must include this method so
+     * we can be informed when the mouse enters a component.
+     * @param event A MouseEvent containing all the information about
+     * this mouse action.
+     */
+    public void mouseEntered(MouseEvent event) {
+    }
+
+    /** Any implementation of MouseListener must include this method so
+     * we can be informed when the mouse exits a component.
+     * @param event A MouseEvent containing all the information about
+     * this mouse action.
+     */
+    public void mouseExited(MouseEvent event) {
+    }
+
+    /** Any implementation of MouseListener must include this method so
+     * we can be informed when the mouse is pressed (start of click).
+     * @param event A MouseEvent containing all the information about
+     * this mouse action.
+     */
+    public void mousePressed(MouseEvent event) {
+    }
+
+    /** Any implementation of MouseListener must include this method so
+     * we can be informed when the mouse is released (end of click).
+     * @param event A MouseEvent containing all the information about
+     * this mouse action.
+     */
+    public void mouseReleased(MouseEvent event) {
+	try {
+	switch(this.type) {
+	case 0: // this.ADD
+	    if(this.temp_path != null && isAppropriate(temp_path)) {
+		super.addSelectionPath(this.temp_path);
+		this.temp_path = null;
+	    }
+	    if(this.temp_paths != null && isAppropriate(temp_paths, true)) {
+		super.addSelectionPaths(this.temp_paths);
+		this.temp_paths = null;
+	    }
+	    this.type = this.NONE;
+	    break;
+	case 1: // this.SET
+	    if(this.temp_path != null) {
+		super.setSelectionPath(this.temp_path);
+		this.temp_path = null;
+	    }
+	    if(this.temp_paths != null && isAppropriate(temp_paths, false)) {
+		super.setSelectionPaths(this.temp_paths);
+		this.temp_paths = null;
+	    }
+	    this.type = this.NONE;
+	    break;
+	}
+	}
+	catch (Exception error) {
+		DebugStream.printStackTrace(error);
+	}
+    }
+
+    public void setImmediate(boolean value) {
+	immediate = value;
+    }
+
+    public void setSelectionPath(TreePath path) {
+	// Since this is only a single path we don't need to check whether its an appropriate selection.
+	if(!immediate) {
+	    this.temp_path = path;
+	    this.type = this.SET;
+	}
+	else {
+	    temp_path = null;
+	    super.setSelectionPath(path);
+	}
+    }
+
+    public void setSelectionPaths(TreePath paths[]) {
+	if(!immediate) {
+	    this.temp_paths = paths;
+	    this.type = this.SET;
+	}
+	else if(isAppropriate(paths, false)) {
+	    temp_paths = null;
+	    super.setSelectionPaths(paths);
+	}
+    }
+
+    /** Ensure that the given path is appropriate to add to the current selection, preventing mixed selections of files and folder if required. We also must check that no path is a ancestor/descendant of another.
+     */
+    private boolean isAppropriate(TreePath path) {
+	boolean appropriate = true;
+	TreeNode new_node = (TreeNode) path.getLastPathComponent();
+	// If there is a current selection
+	if(selection != null && selection.length > 0) {
+	    for(int i = 0; appropriate && i < selection.length; i++) {
+		TreeNode current_node = (TreeNode) selection[i].getLastPathComponent();
+		appropriate = appropriate && (mixed_selection || new_node.isLeaf() == current_node.isLeaf());
+		appropriate = appropriate && !path.isDescendant(selection[i]) && !selection[i].isDescendant(path);
+	    }
+	}
+	return appropriate;
+    }
+    /** Ensure that the given paths are appropriate to add to the current selection, preventing mixed selections of files and folder if required. We also must check that no path is a ancestor/descendant of another. One last detail to keep in mind is that adding selections depends upon the current selection, whereas set the selection paths doesn't (replaces them instead) and thus no check of the current paths is needed. */
+    private boolean isAppropriate(TreePath[] paths, boolean check_current) {
+	boolean appropriate = true;
+	if(check_current && paths != null && paths.length > 0) {
+	    for(int i = 0; appropriate && i < paths.length; i++) {
+		appropriate = appropriate && isAppropriate(paths[i]);
+	    }
+	}
+	return appropriate;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/ExternalProgram.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/ExternalProgram.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/ExternalProgram.java	(revision 31635)
@@ -0,0 +1,107 @@
+package org.greenstone.gatherer.util;
+
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import org.greenstone.gatherer.DebugStream;
+
+
+/**
+ * This class provides a convenient method of running external programs and viewing/analysing
+ * their output. The external programs may be run with or without arguments. 
+ */
+public class ExternalProgram
+{
+    private Process programProcess = null;
+
+    private BufferedReader programOut = null;
+    private BufferedReader programErr = null;
+
+
+    /**
+     * Prepares to run a specified external program with the specified arguments.
+     *
+     * @param programName The name (and path, if necessary) of the program to run.
+     * @param programArgs The arguments to provide to the program when run.
+     */
+    public ExternalProgram(String programName, String programArgs)
+	throws IOException
+    {
+	// Run the program
+	programProcess = Runtime.getRuntime().exec(programName + " " + programArgs);
+
+	// Grab the program output
+	programOut = new BufferedReader(new InputStreamReader(programProcess.getInputStream()));
+	programErr = new BufferedReader(new InputStreamReader(programProcess.getErrorStream()));
+
+    }
+
+
+
+    /**
+     */
+    public int exitProgram()
+	throws IOException
+    {
+	// Finished, thank you
+	//programIn.close();
+
+	// Wait for the process to finish
+	try {
+	    programProcess.waitFor();
+	}
+	catch (InterruptedException ex) {
+	    throw new Error("Internal Error: Process interrupted.\n" + ex);
+	}
+
+	// Return the program exit value
+	return programProcess.exitValue();
+    }
+
+
+    /**
+     * Returns a single line of output sent by the program to standard output.
+     *
+     * @return a single line of output sent by the program to standard output.
+     */
+    public String getLineOfProgramOutput()
+    {
+	// Read in a line from the program output
+	try {
+	    if (programOut.ready())
+		return programOut.readLine();
+	    else
+		return null;
+	}
+	catch (IOException ex) {
+	    DebugStream.println("Error: Exception occurred while reading program output.");
+	    DebugStream.println("Exception: " + ex);
+	    return null;
+	}
+    }
+
+
+    /**
+     * Returns a single line of output sent by the program to standard error.
+     *
+     * @return a single line of output sent by the program to standard error.
+     */
+    public String getLineOfProgramError()
+    {
+	// Read in a line from the program error
+	try {
+	    if (programErr.ready())
+		return programErr.readLine();
+	    else
+		return null;
+	}
+	catch (IOException ex) {
+	    DebugStream.println("Error: Exception occurred while reading program error.");
+	    DebugStream.println("Exception: " + ex);
+	    return null;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/GLIEntityResolver.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/GLIEntityResolver.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/GLIEntityResolver.java	(revision 31635)
@@ -0,0 +1,191 @@
+/*
+ *    GLIEntityResolver.java
+ *    Copyright (C) 2008 New Zealand Digital Library, http://www.nzdl.org
+ *
+ *    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 2 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, write to the Free Software
+ *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+package org.greenstone.gatherer.util;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Gatherer;
+
+import org.xml.sax.InputSource;
+import org.xml.sax.EntityResolver;
+import java.io.File;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+
+/**
+ * Uses a list of default search paths, or the file's own path if provided, to resolve entities referenced 
+ * in the file. The search paths, including the file's own, have to be within GS.
+ * By default, the GS3HOME/WEB-INF and toplevel GLI user directory are added to the list of search paths,
+ * since this EntityResolver is used for web.xml to resolve its inclusion of servlets.xml, which can be
+ * located in GS3HOME/WEB-INF for a local GLI or is extracted into the gli user directory for client-GLI.
+ * If everything fails, it will try to resolve entities using the classloader, not otherwise used by GLI,
+ * but this part has been copied over from GS3's GSEntityResolver.java.
+ * If ever needed, maybe to make things faster, can maintain a static list of default search paths.
+ */
+public class GLIEntityResolver implements EntityResolver {
+
+    protected ArrayList<File> list_of_local_search_paths = new ArrayList<File>();
+    ClassLoader class_loader = null;
+
+
+    public GLIEntityResolver() {
+	// Add the basic search paths:
+
+	// add the GS3 web/WEB-INF folder as search location for servlets.xml, which web.xml 
+	// includes and which is the reason for adding a GLIEntityResolver class into GLI
+
+	if(Gatherer.GS3) {
+	    //list_of_local_search_paths.add(new File(Configuration.gsdl3_path));
+	    list_of_local_search_paths.add(new File(Configuration.gsdl3_path, "WEB-INF"));
+	}
+
+	// Add gli user dir for remote GS, since that is where the
+	// web.xml and server.xml downloaded from the remote server will be unpacked to
+	if(Gatherer.isGsdlRemote) {
+	    list_of_local_search_paths.add(new File(Configuration.gli_user_directory_path));
+	}
+    }
+
+    public GLIEntityResolver(File file) {
+	this(); // adds default search paths
+
+	// add file's own path as first in search list
+	addSafeSearchPath(file, true);	
+    }
+
+    /* Methods with the ClassLoader parameter are unused at present */
+    public GLIEntityResolver(ClassLoader loader) {
+	this.class_loader = loader;
+    }
+
+    public GLIEntityResolver(File f, ClassLoader loader) {
+	this(f);
+	this.class_loader = loader;
+    }
+
+    public void setClassLoader(ClassLoader loader) {
+	this.class_loader = loader;
+    }
+    
+    public void addSafeSearchPath(File file, boolean prepend) {
+	
+	// add the file's directory to list of search paths,
+	// if it is within the greenstone installation and not already in the list
+	if((!Gatherer.GS3 && file.getAbsolutePath().startsWith(Configuration.gsdl_path))
+	   || (Gatherer.GS3 && file.getAbsolutePath().startsWith(Configuration.gsdl3_src_path))) {
+
+	    File path = file.getParentFile();
+	    if(!list_of_local_search_paths.contains(path)) {
+		if(prepend) {
+		    list_of_local_search_paths.add(0, path);
+		} else {
+		    list_of_local_search_paths.add(path);
+		}
+	    }
+	} else {
+	    DebugStream.println("### Location of file " + file + " not within GS. Not adding to list of search paths.");
+	}
+    }
+
+    /**
+     * resolveEntity() is not called for every file:
+     * http://www.postseek.com/meta/37735b65e6a459a6aa631f048cc5a0b6
+     * 
+     *  "I think the parser will call [resolveEntity() on an EntityResolver that has been set], if 
+     *  [the parser] is unable to find the DTD that your XML file refers to. So if you try to parse 
+     *  an XML file that doesn't refer to a DTD, it won't be called. And if you try to parse an XML 
+     *  file where the parser can find the DTD, it won't be called either."
+     * 
+    */
+    public InputSource resolveEntity (String public_id, String system_id) {
+	
+	DebugStream.println("### resolveEntity() called for " + system_id);
+
+	String temp_id = system_id;
+	if (temp_id.startsWith("file://")) {
+	    File f = new File(system_id);
+	    if (f.exists()) {		
+
+		// check if inside GS2 or GS3 installation
+		if(f.getAbsolutePath().startsWith(Configuration.gsdl_path) 
+		   || f.getAbsolutePath().startsWith(Configuration.gsdl3_src_path)) {
+		    
+		    DebugStream.println("### file denoted by systemID is inside GS: " + f.getAbsolutePath());
+
+		    return new InputSource(system_id); // problem solved
+		} else {
+		    DebugStream.println("\t### file denoted by systemID exists, but not located inside GS: " 
+				       + f.getAbsolutePath());
+		}
+	    } else {
+		
+	        temp_id = f.getName();
+		
+		//check in list of search paths
+		Iterator<File> i = list_of_local_search_paths.iterator();
+		while(i.hasNext()) {
+		    File searchPath = i.next();
+		    DebugStream.println("### searching for entity '" + temp_id + "' in: "
+				       + searchPath.getAbsolutePath());
+
+		    File searchFile = new File(searchPath, temp_id);
+		    
+		    if(searchFile.exists()) {
+				DebugStream.println("\t Found " + searchFile.getAbsolutePath());				
+			
+				String newpath = searchFile.getAbsolutePath();		
+				if(Utility.isWindows()) {			
+					newpath = "file:///" + newpath.replace("\\", "/");
+				} else { // linux version, file protocol starts with file:// and slashes are already URL-style
+					newpath = "file://" + newpath;
+				}
+				return new InputSource(newpath);
+				
+		    }
+		}
+
+		// else, external entity/file denoted by systemid is not in list of search paths, try classloader
+		
+	    }
+	} else {
+
+	    DebugStream.println("### Entity is not a file: " + system_id);
+
+	    if (temp_id.indexOf("/")!= -1) {
+		temp_id = temp_id.substring(temp_id.lastIndexOf("/")+1);
+	    }
+	}
+
+	DebugStream.println("### Using classloader to attempt to resolve entity: " + temp_id);
+
+	// try using a class loader. If none provided, use current class loader
+	if (this.class_loader==null) { 
+	    this.class_loader = this.getClass().getClassLoader();
+	}
+	URL url = class_loader.getResource(temp_id);
+	if (url == null) {
+	    return null;
+	}
+
+	return new InputSource("file://"+url.getFile());
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/GS3ServerThread.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/GS3ServerThread.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/GS3ServerThread.java	(revision 31635)
@@ -0,0 +1,147 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: Sam McIntosh, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 2011 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.util;
+
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+
+import java.io.File;
+
+/** Class for GS3, for issuing ant commands from GLI */
+public class GS3ServerThread extends Thread
+{
+    String _gsdl3_src_path = "";
+    String _ant_command = "";
+    
+    public GS3ServerThread(String gsdl3_src_path, String ant_command)
+    {
+	_gsdl3_src_path = gsdl3_src_path;
+	_ant_command = ant_command; // "restart"
+    }
+    
+
+    public void run()
+    {
+
+	///System.err.println("**** GS3 server : " + _ant_command);
+
+	SafeProcess p = null;
+	if (Utility.isWindows()) {
+	    if(_ant_command.indexOf("start") != -1) { // running an "ant (re)start" command on windows, run start
+		_ant_command = "start";
+	    }
+	    
+	    // The path in quotes, and the entire sequence of commands in quotes as well
+	    // E.g. the following works in a Runtime.exec() call:
+	    // cmd /C "cd "C:\path\to\greenstone3" && ant stop"
+	    // and it preserves any spaces in the path to GSDL3SRCHOME (_gsdl3_src_path).
+	    p = new SafeProcess("cmd /C \"cd \"" + _gsdl3_src_path + File.separator + "\" && ant " + _ant_command + "\"");
+	}
+	else {
+	    if(_ant_command.indexOf("start") != -1) { // if running an "ant (re)start" command on non-Windows, run restart
+		_ant_command = "restart";
+	    }
+	    p = new SafeProcess(new String[]{"/bin/bash", "-c", "ant " + _ant_command + " -f \"" + _gsdl3_src_path + File.separator + "build.xml\""});
+	}
+	
+	
+	// in order for the process.waitFor() method to work with Java 6 (JRE 6 is included in GS binaries)
+	// need to make sure the IOstreams of the process are not blocked. For Java 7, this is not necessary
+	// and a waitFor() is sufficient. But with Java 6, the waitFor() causes the server to finally start
+	// after the user has quit GLI.
+	// Process takes no input, but we will still catch the process' instream too
+	// And we'll catch the error and output streams to prevent them from blocking during waitFor()
+	// (For normal input and output stream handling using the Gobblers, see FormatConversionDialog.java)
+
+
+	// prepare our SafeProcess object
+	p.setSplitStdErrorNewLines(true);
+
+	// run it
+	int result = p.runProcess(); // uses default process streamgobbler behaviours and
+	// does the important part: waitFor() the process (ant stop or start or re-start) to terminate.
+	// The int result returned is the exitvalue upon Process.waitFor() returning
+	
+	if(result != 0) {
+	    System.err.println("**** Failed to successfully " + _ant_command + " the GS3 server.");
+	}
+	///else {
+	///System.err.println("**** " + _ant_command + " of the GS3 server successful.");
+	///}
+	
+    }
+
+
+    // can't call ant stop from its own thread - what if GLI has exited by then?
+    // issue call to ant stop from the main GLI thread
+    //GS3ServerThread thread = new GS3ServerThread(Configuration.gsdl_path, "stop");
+    //thread.start();
+    // So, static function to issue the command to stop the server from GLI's own thread.
+    // This will block the main GLI thread until the server has stopped.
+    public static void stopServer() {
+
+	SafeProcess p = null;
+	if (Utility.isWindows()) {
+	    // cmd /C "cd "C:\path\to\greenstone3" && ant stop"
+	    p = new SafeProcess("cmd /C \"cd \"" + Configuration.gsdl3_src_path + File.separator + "\" && ant stop\"");	
+	} else {
+	    p = new SafeProcess(new String[]{"/bin/bash", "-c", "ant stop -f \"" + Configuration.gsdl3_src_path + File.separator + "build.xml\""});
+	}
+
+	System.err.println("Issuing stop command to GS3 Server. Waiting for GS3 server to stop...");
+	int result = p.runProcess();
+	if(result == 0) {
+	    System.err.println("Successfully stopped GS3 server.");
+	    //DebugStream.println("********** SUCCESSFULLY stopped THE GS3 SERVER ON EXIT");
+	}
+	else {
+	    System.err.println("********** FAILED TO SUCCESSFULLY stop THE GS3 SERVER ON EXIT");
+	}
+	
+	// doing a p.waitFor() without processing the Process' IOstreams causes blocking with Java 6 
+	// (i.e. when JRE 6 included with GS binaries). However, p.waitFor() with Java 7 is fine.
+	/*if(p != null && p.waitFor() == 0) {
+	  DebugStream.println("********** SUCCESSFULLY stopped THE GS3 SERVER ON EXIT");
+	  }
+	  else {
+	  System.err.println("********** FAILED TO SUCCESSFULLY stop THE GS3 SERVER ON EXIT");
+	  //throw new Exception ("Failed to successfully stop the GS3 server on exit.");
+	  }*/
+	
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/HTMLStringTokenizer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/HTMLStringTokenizer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/HTMLStringTokenizer.java	(revision 31635)
@@ -0,0 +1,142 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+
+
+/** This class functions much like a <strong>StringTokenizer</strong> in that it tokenizes a long string into tokens, however this tokenizer cleverly notices HTML formatting tags. */
+public class HTMLStringTokenizer {
+    /** The current position in the source string. */
+    private int pos = 0;
+    /** The current token, usually created by the last nextToken call. */
+    private String current = null;
+    /** The previous token. */
+    private String previous = null;
+    /** The string to be tokenized, including any HTML markup. */
+    private String source = null;
+    /** Constructor.
+     * @param source The source <strong>String</strong> to be tokenized.
+     */
+    public HTMLStringTokenizer(String source) {
+	this.source = source;
+	// Parse the first token.
+	parseToken();
+    }
+
+    /** Determines if there are still tokens remaining unparsed in the source.
+     * @return A <strong>boolean</strong> which is <i>true</i> if there are more tokens.
+     */ 
+    public boolean hasMoreTokens() {
+	if(current != null && current.length() > 0) {
+	    return true;
+	}
+	return false;
+    } 
+
+    /** Determines if the tag currently being returned by sameToken is a tag.
+     * @return A <strong>boolean</strong> indicating if the token is a tag.
+     */
+    public boolean isTag() {
+	if(previous.startsWith("<") && previous.endsWith(">")) {
+	    return true;
+	}
+	return false;
+    }
+
+    /** Retrieves the next token. 
+     * @return A <strong>String</strong> representing the token.
+     */
+    public String nextToken() {
+	previous = current;
+	// Get the next token.
+	parseToken();
+	// Return previous.
+	return previous;
+    }
+
+    /** Parses the next token and stores it in current.
+     */
+    private void parseToken() {
+	boolean found = false;
+	boolean tag = false;
+	boolean text = false;
+	// Reset current
+	current = "";
+	// Parse away
+	dumpWhiteSpace();
+	while(pos < source.length() && !found) {
+	    char c = (char)source.charAt(pos);
+	    if(!tag && !text) {
+		if(c == '<') {
+		    tag = true;
+		}
+		else {
+		    text = true;
+		}
+		current = current + c;
+	    }
+				// Reading a tag. Watch only for '>'.
+	    else if(tag) {
+		if(c == '>') {
+		    found = true;
+		}
+		current = current + c;
+	    }
+				// Reading text. Watch for ' ' and '<'. Rollback '<'.
+	    else if(text) {
+		if(c == ' ') {
+		    found = true;
+		}
+		else if(c == '<') {
+		    found = true;
+		    pos--;
+		}
+		else {
+		    current = current + c;
+		}
+	    }
+	    pos++;
+	}
+    }
+
+    /** Method to ignore whitespace in the source.
+     */
+    private void dumpWhiteSpace() {
+	while(pos < source.length() && source.charAt(pos) == ' ') {
+	    pos++;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/HashMap3D.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/HashMap3D.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/HashMap3D.java	(revision 31635)
@@ -0,0 +1,138 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+
+/**************************************************************************************
+ * Written:      20/08/02
+ * Revised:      
+ **************************************************************************************/
+import java.util.HashMap;
+import java.util.Iterator;
+
+/** Provides a HashMap implementation that indexes by two keys. Perfect for the storage of metadata references based on their metadata element and assigned value. 
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class HashMap3D
+    extends HashMap {
+
+    private int capacity = 4;
+
+    private int size = 0;
+
+    private Object last_key_one = null;
+    private Object last_key_two = null;
+    private Object last_value = null;
+
+    /** Default constructor.
+     */
+    public HashMap3D() {
+	super();
+    }
+    /** Constructor with a specific initial capacity.
+     * @param capacity The initial capacity as an <i>int</i>.
+     */
+    public HashMap3D(int capacity) {
+	super(capacity);
+	this.capacity = capacity;
+    }
+    /** Completely remove the contents of this HashMap and its child HashMaps. */
+    public void clear() {
+	size = 0;
+	Iterator iterator = values().iterator();
+	while(iterator.hasNext()) {
+	    HashMap inner_mapping = (HashMap) iterator.next();
+	    inner_mapping.clear();
+	    inner_mapping = null;
+	}
+	iterator = null;
+	super.clear();
+    }
+    /** Determine if this hash map contains an entry for the given keys. Also cache this entry because theres a good chance the next get call will ask for this entry.
+     * @param key_one The first key as an <strong>Object</strong>.
+     * @param key_two The second key as an <strong>Object</strong>.
+     * @return <i>true</i> if such an entry exists, <i>false</i> otherwise.
+     */
+    public boolean contains(Object key_one, Object key_two) {
+	boolean result = false;
+	// Retrieve the hash mapping at key_one.
+	HashMap map = (HashMap) get(key_one);
+	// If there is such a map then retrieve the value at key_two.
+	if(map != null) {
+	    last_value = map.get(key_two);
+	    if(last_value != null) {
+		last_key_one = key_one;
+		last_key_two = key_two;
+		result = true;
+	    }
+	}
+	return result;
+    }
+
+    /** Retrieve an entry from this three dimensional hash mapping.
+     * @param key_one The first key as an <strong>Object</strong>.
+     * @param key_two The second key as an <strong>Object</strong>.
+     * @return The value <strong>Object</strong> located at (key_one, key_two), or <i>null</i> if no such value.
+     */
+    public Object get(Object key_one, Object key_two) {
+	Object result = null;
+	if(key_one.equals(last_key_one) && key_two.equals(last_key_two)) {
+	    result = last_value;
+	}
+	else {
+	    // Retrieve the hash mapping at key_one.
+	    HashMap map = (HashMap) get(key_one);
+	    // If there is such a map then retrieve the value at key_two.
+	    if(map != null) {
+		result = map.get(key_two);
+		if(result != null) {
+		    last_key_one = key_one;
+		    last_key_two = key_two;
+		    last_value = result;
+		}
+	    }
+	}
+	return result;
+    }
+
+    /** Put an entry into this three dimensional hash mapping.
+     * @param key_one The first key, to store this value under, as an <strong>Object</strong>.
+     * @param key_two The second key, to store this value under, as an <strong>Object</strong>.
+     * @param value The value <strong>Object</strong> itself.
+     */
+    public void put(Object key_one, Object key_two, Object value) {
+	// Retrieve the hash mapping at key_one, or if none exists create one.
+	HashMap map = (HashMap) get(key_one);
+	if(map == null) {
+	    map = new HashMap(capacity);
+	    put(key_one, map);
+	}
+	// Now add the value to this mapping.
+	map.put(key_two, value);
+	size++;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/JarTools.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/JarTools.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/JarTools.java	(revision 31635)
@@ -0,0 +1,141 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.util;
+
+
+import java.awt.*;
+import java.io.*;
+import java.net.*;
+import javax.swing.*;
+import org.greenstone.gatherer.DebugStream;
+
+
+public class JarTools
+{
+    static private Class root_class = null;
+    static private ImageIcon ERROR_ICON = null; 
+
+
+    static public void initialise(Object root_object)
+    {
+	root_class = root_object.getClass();
+	ERROR_ICON = getImage("error.gif");
+    }
+
+
+    static public void extractFromJar(String filename, String dst_dir, boolean must_be_present) 
+    {
+	try {
+	    // setup input stream for slurping out file
+	    InputStream fis = root_class.getResourceAsStream("/"+filename);
+	    BufferedInputStream fbis = new BufferedInputStream(fis); 
+	    DataInputStream fdbis = new DataInputStream(fbis);
+
+	    // set up output stream for writing to disk
+	    String ofname = dst_dir + filename;
+	    FileOutputStream fos = new FileOutputStream(ofname);
+	    BufferedOutputStream bfos = new BufferedOutputStream(fos);
+
+	    byte[] buf = new byte[1024];
+	    int len;
+	    int total_bytes = 0;
+	    while ((len = fdbis.read(buf)) >= 0) {
+		bfos.write(buf,0,len);
+		total_bytes += len;
+	    }
+
+	    fdbis.close();
+	    bfos.close();
+	}
+	catch (Exception exception) {
+	    if (must_be_present) {
+		exception.printStackTrace();
+	    }
+	}
+    }
+
+
+    /** Method to retrieve an image icon with the given filename found in classpath or the resouces directory.
+     *  @return The specified <strong>ImageIcon</strong>, or an error image replacement if no such images exists.
+     */
+    static public ImageIcon getImage(String filename) {
+	return getImage(filename, false);
+    }
+
+    static public ImageIcon getImage(String filename, boolean wait_until_complete)
+    {
+	ImageIcon image = null;
+	try {
+	    image = new ImageIcon(root_class.getResource("/images/" + filename));
+	}
+	catch (NullPointerException exception) {
+	    System.err.println("Error: Could not load image " + filename);
+	    DebugStream.println("Error: Could not load image " + filename);
+	}
+
+	if (image == null) {
+	    image = ERROR_ICON;
+	}
+
+	if (wait_until_complete) {
+	    int load_status;
+	    do {
+		load_status = image.getImageLoadStatus();
+	    }
+	    while (load_status != MediaTracker.ABORTED && load_status != MediaTracker.ERRORED && load_status != MediaTracker.COMPLETE);
+	}
+
+	return image;
+    }
+
+
+    static public URL getResource(String resource_name)
+    {
+	return root_class.getResource(resource_name);
+    }
+
+
+    static public InputStream getResourceAsStream(String resource_name)
+    {
+	return root_class.getResourceAsStream(resource_name);
+    }
+
+
+    static public boolean isInJar(String filename) 
+    {
+	try {
+	    InputStream fis = root_class.getResourceAsStream("/" + filename);		    
+	    fis.close();
+	}
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	    return false;
+	}
+
+	return true;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/PatternTokenizer.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/PatternTokenizer.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/PatternTokenizer.java	(revision 31635)
@@ -0,0 +1,72 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+
+public class PatternTokenizer {
+    private int index;
+    private String content;
+    private String pattern;
+    public PatternTokenizer(String content, String pattern) {
+	this.content = content;
+	this.index = 0;
+	this.pattern = pattern;
+    }
+    public boolean hasMoreTokens() {
+	return (index != -1 && content.length() - index > 0);
+    }
+    public String nextToken() {
+	// Special case when index is zero
+	if(index == 0) {
+	    index = content.indexOf(pattern);
+	    // No occurance of pattern, return whole string
+	    if(index == -1) {
+		return content;
+	    }
+	    // Return the fragment from 0 to index
+	    else {
+		return content.substring(0, index);
+	    }
+	}
+	// Otherwise
+	else {
+	    // We must first skip the pattern, as it should be at the head of our current view into content
+	    index = index + pattern.length();
+	    int end_index = content.indexOf(pattern, index);
+	    String fragment;
+	    // No occurance of pattern, return fragment from index to length
+	    if(end_index == -1) {
+		fragment = content.substring(index);
+	    }
+	    // Return the fragment from index to end index, then set index to end index
+	    else {
+		fragment = content.substring(index, end_index);
+	    }
+	    index = end_index;
+	    return fragment;
+	}
+    }    
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/PortFinder.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/PortFinder.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/PortFinder.java	(revision 31635)
@@ -0,0 +1,136 @@
+package org.greenstone.gatherer.util;
+
+/** Verbatim copy of what's in GS3's org.greenstone.server */
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.BindException;
+
+// When needing to use sockets, this class can be used to find an available port
+public class PortFinder {
+
+    // Port numbers range from 0 to 65,535. RESERVED PORTS are below 1025
+    // see http://www.gnu.org/software/hello/manual/libc/Ports.html
+    public static final int MAX_PORT = 65535;
+    public static final int PORTS_RESERVED = 1024;
+
+    // Considering a limited range of ports that will be reused (circular buffer)
+    public final int PORT_BASE;
+    public final int PORT_BLOCK_SIZE;
+
+    // Keep track of what port numbers we have checked for availability
+    private int nextFreePort;
+
+    // The socket port number that we will use
+    private int port = -1;
+
+
+    public PortFinder() {
+	this( PORTS_RESERVED+1, (MAX_PORT - PORTS_RESERVED) );
+    }
+
+    public PortFinder(int base, int blocksize) {
+	PORT_BASE = base;
+	PORT_BLOCK_SIZE = blocksize;
+
+	nextFreePort = PORT_BASE;
+    }
+
+    /** Circular buffer. Modifies the value of nextFreePort (the buffer index). */
+    private void incrementNextFreePort() {
+	int offset = nextFreePort - PORT_BASE;
+	offset = (offset + 1) % PORT_BLOCK_SIZE;
+	nextFreePort = PORT_BASE + offset;
+    } 
+
+    /** Finds the first available port in the range specified during Constructor call.
+     *  @return the number of an available port.	
+     */
+    public int findPortInRange(boolean verbose) throws Exception {	
+	try {
+	    boolean foundFreePort = false;
+	    for(int i = 0; i < PORT_BLOCK_SIZE; i++) {
+		
+		if(isPortAvailable(nextFreePort, verbose)) {
+		    foundFreePort = true;
+		    break;
+		    
+		} else {
+		    incrementNextFreePort();
+		}
+	    }
+	    
+	    if(foundFreePort) {
+		// Free port number currently found becomes the port number of the socket
+		// that will be used
+		this.port = nextFreePort;
+		incrementNextFreePort();
+		
+	    } else {
+		throw new Exception("Cannot find an available port in the range " 
+				    + PORT_BASE + "-" + (PORT_BASE+PORT_BLOCK_SIZE));
+	    }
+	    
+	} catch(IOException e) {
+	    System.err.println("Error when trying to find an available port. In PortFinder.findPort() " + e);
+	}
+	return port;
+    }
+
+    /** @return true if the portnumber is in the valid range. */
+    public static boolean isValidPortNumber(int portNum) {
+	return (portNum >= 0 && portNum <= MAX_PORT);
+    }
+
+    /** @return true if the portnumber is in the useable range. */
+    public static boolean isAssignablePortNumber(int portNum) {
+	return (portNum > PORTS_RESERVED && portNum <= MAX_PORT);
+    }
+
+    /** Finds an available port.
+     * @return the number of an available port.
+     */
+    public static int findAnyFreePort() throws Exception {	
+	ServerSocket tmpSocket = null;
+	int portNumber = -1;
+	try {
+	    // Creates a server socket, bound to the specified port.
+	    // A port of 0 creates a socket on any free port. 
+	    tmpSocket = new ServerSocket(0);
+	    portNumber = tmpSocket.getLocalPort();
+	    tmpSocket.close();
+	} catch(Exception e) {
+	    System.err.println("Unable to find a free port or close it. Got Exception: " + e);
+	    tmpSocket = null;
+	}
+	return portNumber;
+    }
+
+    /** @return true if the portnum is available for use */
+    public static boolean isPortAvailable(int portnum, boolean verbose) {
+	ServerSocket tmpSocket = null;
+	try {
+	    tmpSocket = new ServerSocket(portnum);
+	    tmpSocket.close();
+	    //if(verbose) {
+	    //System.err.println("Port " + portnum + " not yet in use.");
+	    //}
+	    return true;
+	    
+	} catch(BindException ex){
+	    // "Signals that an error occurred while attempting to bind a 
+	    // socket to a local address and port. Typically, the port is 
+	    // in use, or the requested local address could not be assigned."
+	    if(verbose) {
+		System.err.println("Port " + portnum + " already in use or can't be assigned.");
+	    }
+	    tmpSocket = null;
+	    return false;	    
+	} catch(Exception ex) { 
+	    // Some other problem creating or closing the server socket
+	    System.err.println("Problem creating or closing server socket at port " + portnum);
+	    tmpSocket = null;
+	    return false;
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/RemoveContentBeforeRootElementXMLReader.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/RemoveContentBeforeRootElementXMLReader.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/RemoveContentBeforeRootElementXMLReader.java	(revision 31635)
@@ -0,0 +1,186 @@
+package org.greenstone.gatherer.util;
+
+import java.io.Reader;
+import java.io.IOException;
+
+import java.io.File;
+import java.io.FileReader;
+
+import java.util.regex.Pattern;
+
+public class RemoveContentBeforeRootElementXMLReader extends Reader {
+
+  static final Pattern[] xmlIndicators;
+  static Pattern commentStart;
+  static Pattern commentStop;
+
+  static {
+    // generate the xml starting sequences we will try to match against
+    xmlIndicators = new Pattern[3];
+    try{
+      xmlIndicators[0] = Pattern.compile("<\\?xml"); //usual xml declaration
+      xmlIndicators[1] = Pattern.compile("<!DOCTYPE"); //doctype declaration
+      xmlIndicators[2] = Pattern.compile("<[a-zA-Z-]+[ >/]"); // the beginning of a root node
+
+      commentStart = Pattern.compile("<!--");
+      commentStop = Pattern.compile("-->");
+
+    } catch ( java.util.regex.PatternSyntaxException pse ) {
+        System.err.println( "Pattern no good. " + pse );
+        xmlIndicators[0] = xmlIndicators[1] = xmlIndicators[2] = null;
+        commentStart = commentStop = null;
+    }
+  }
+
+  Reader ur;
+
+  String finalBuffer = null;
+  int finalBufferIndex = 0;
+
+  public RemoveContentBeforeRootElementXMLReader( Reader ur ) {
+
+    this.ur = ur;
+
+    int found = -1;
+    boolean inComment = false;
+    StringBuffer buffer = null;
+
+    for ( int c = 0;  c != -1 && found == -1; ) {
+
+      //read a character
+      try {
+        c = ur.read();
+      } catch( Exception e ) {
+        System.err.println( "Exception while reading underlying Reader in RemoveContentBeforeRootElementXMLReader" );
+      }
+
+      //break out if we have reached the end of the input
+      if ( c == -1 ) {
+        break;
+      }
+
+      //we start buffering when we come across the first <
+      //regardless of whether it turns out to be a relevant < or not
+      if ( buffer == null && (char)c == '<' )  {
+        buffer = new StringBuffer();
+      }
+
+      //if not buffering, just display the character and move onto next character
+      if ( buffer == null ) {
+         System.err.print( (char)c );
+        continue;
+      }
+
+      buffer.append( (char)c );
+
+      //check for comment open or close
+      if ( !inComment ) {
+        if ( commentStart.matcher(buffer.toString()).find() ) {
+          inComment = true;
+          System.err.print( buffer.toString() );
+          buffer = new StringBuffer();
+        }
+      } else {
+        if ( commentStop.matcher(buffer.toString()).find() ) {
+          inComment = false;
+          System.err.print( buffer.toString() );
+          buffer = new StringBuffer();
+
+          //skip to reading next character
+          continue;
+        }
+      }
+
+      if ( !inComment ) {
+
+        //check each indicator to see if found
+        for ( int i = 0; i < xmlIndicators.length && found == -1; i++ ) {
+          if ( xmlIndicators[i].matcher(buffer.toString()).find() ) {
+            found = i;
+            String line = buffer.toString();
+            int lastIndex = line.lastIndexOf('<');
+            //flush the previous characters in the buffer to the console
+            System.err.print(line.substring(0, lastIndex));
+            buffer.delete(0, lastIndex);
+            finalBuffer = buffer.toString();
+          }
+        }
+
+      }
+    }
+
+    if ( found == -1 ) {
+      System.err.println( "RemoveContentBeforeRootElementXMLReader:\n" +
+        "The XML being loaded was not valid: couldn't find start of XML input" );
+    }
+  
+  }
+
+  public int read( char[] cbuf, int off, int len ) throws IOException {
+
+    for ( int i=off; i<off+len && i<cbuf.length; i++ ) {
+
+      //read from underlying reader
+      int c = read();
+
+      //catch end of stream
+      if ( c == -1 ) {
+        if ( i == off ) {
+          return -1;
+        }
+        return i - off;
+      }
+
+      //insert character into the array
+      cbuf[i] = (char)c;
+    }
+    return len;
+
+  }
+
+  public int read() throws IOException {
+
+    //flush the buffer containing the opening XML sequence
+    if ( finalBuffer != null && finalBuffer.length() > finalBufferIndex ) {
+      char c = finalBuffer.charAt(finalBufferIndex++);
+      if ( finalBufferIndex == finalBuffer.length() ) {
+        finalBuffer = null;
+      }
+      return c;
+    }
+
+    return ur.read();
+  }
+
+  public void close() throws IOException {
+    ur.close();
+  }
+
+  public static void main ( String[] args ) {
+
+    //init
+    System.out.println( "------------\nWill now initialise the test reader\n------------" );
+    RemoveContentBeforeRootElementXMLReader parser = null;
+    try {
+      parser = new RemoveContentBeforeRootElementXMLReader( 
+        new FileReader( new File("text.xml") ) );
+    } catch ( java.io.FileNotFoundException fnfe ) {
+      System.err.println( "Please create text.xml to test this class" );
+      System.exit(-1);
+    }
+
+    //read the rest of the input
+    System.out.println( "------------\nWill now read the rest of the input\n------------" );
+    try {
+      int c = 0;
+      while ( ( c = parser.read() ) != -1 ) {
+        System.out.print( (char)c );
+      }
+    } catch ( Exception e ) {
+      System.err.println("Exception: " + e);
+    }
+    
+  }
+
+
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/SafeProcess.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/SafeProcess.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/SafeProcess.java	(revision 31635)
@@ -0,0 +1,677 @@
+package org.greenstone.gatherer.util;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+import java.util.Arrays;
+
+import org.apache.log4j.*;
+
+// Use this class to run a Java Process. It follows the good and safe practices at
+// http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
+// to avoid blocking problems that can arise from a Process' input and output streams.
+
+public class SafeProcess {
+
+    public static final int STDERR = 0;
+    public static final int STDOUT = 1;
+    public static final int STDIN = 2;    
+
+    ///static Logger logger = Logger.getLogger(org.greenstone.util.SafeProcess.class.getName());
+
+    // input to SafeProcess and initialising it
+    private String command = null;
+    private String[] command_args = null;
+    private String[] envp = null;
+    private File dir = null;
+    private String inputStr = null;
+
+    // output from running SafeProcess.runProcess()
+    private String outputStr = ""; 
+    private String errorStr = "";
+    private int exitValue = -1;
+
+    // allow callers to process exceptions of the main process thread if they want
+    private ExceptionHandler exceptionHandler = null;
+
+    // whether std/err output should be split at new lines
+    private boolean splitStdOutputNewLines = false;
+    private boolean splitStdErrorNewLines = false;
+
+    // call one of these constructors
+
+    // cmd args version
+    public SafeProcess(String[] cmd_args)
+    {
+	command_args = cmd_args;
+    }
+
+    // cmd string version
+    public SafeProcess(String cmdStr)
+    {
+	command = cmdStr;
+    }
+
+    // cmd args with env version, launchDir can be null.
+    public SafeProcess(String[] cmd_args, String[] envparams, File launchDir)
+    {
+	command_args = cmd_args;
+	envp = envparams;
+	dir = launchDir;
+    }
+
+    // The important methods:
+    // to get the output from std err and std out streams after the process has been run
+    public String getStdOutput() { return outputStr; }
+    public String getStdError() { return errorStr; }
+    public int getExitValue() { return exitValue; }
+
+    
+    // set any string to send as input to the process spawned by SafeProcess
+    public void setInputString(String sendStr) {
+	inputStr = sendStr;
+    }
+
+    // register a SafeProcess ExceptionHandler whose gotException() method will
+    // get called for each exception encountered
+    public void setExceptionHandler(ExceptionHandler exception_handler) {
+	exceptionHandler = exception_handler;	
+    }
+
+    // set if you want the std output or err output to have \n at each newline read from the stream
+    public void setSplitStdOutputNewLines(boolean split) {
+	splitStdOutputNewLines = split;
+    }
+    public void setSplitStdErrorNewLines(boolean split) {
+	splitStdErrorNewLines = split;
+    }
+
+    private Process doRuntimeExec() throws IOException {
+	Process prcs = null;
+	Runtime rt = Runtime.getRuntime();
+	
+	if(this.command != null) {
+	    //logger.info("SafeProcess running: " + command);
+	    System.err.println("SafeProcess running: " + command_args);
+	    prcs = rt.exec(this.command);
+	}
+	else { // at least command_args must be set now
+	    
+	    // http://stackoverflow.com/questions/5283444/convert-array-of-strings-into-a-string-in-java
+	    //logger.info("SafeProcess running: " + Arrays.toString(command_args));
+	    System.err.println("SafeProcess running: " + Arrays.toString(command_args));
+	    
+	    if(this.envp == null) { 
+		prcs = rt.exec(this.command_args);
+	    } else { // launch process using cmd str with env params		
+		
+		if(this.dir == null) {
+		    ///logger.info("\twith: " + Arrays.toString(this.envp));
+		    ///System.err.println("\twith: " + Arrays.toString(this.envp));
+		    prcs = rt.exec(this.command_args, this.envp);
+		} else {
+		    ///logger.info("\tfrom directory: " + this.dir);
+		    ///logger.info("\twith: " + Arrays.toString(this.envp));
+		    ///System.err.println("\tfrom directory: " + this.dir);
+		    ///System.err.println("\twith: " + Arrays.toString(this.envp));
+			prcs = rt.exec(this.command_args, this.envp, this.dir);
+		}
+	    }
+	}
+
+	return prcs;
+    }
+
+    // Copied from gli's gui/FormatConversionDialog.java
+    private int waitForWithStreams(Process prcs,
+				   SafeProcess.OutputStreamGobbler inputGobbler,
+				   SafeProcess.InputStreamGobbler outputGobbler,
+				   SafeProcess.InputStreamGobbler errorGobbler)
+	throws IOException, InterruptedException
+    {
+
+            // kick off the stream gobblers
+            inputGobbler.start();
+            errorGobbler.start();
+            outputGobbler.start();
+
+            // any error???
+            this.exitValue = prcs.waitFor(); // can throw an InterruptedException if process did not terminate
+
+            ///logger.info("Process exitValue: " + exitValue);
+	    ///System.err.println("Process exitValue: " + exitValue); 
+
+	    // From the comments of 
+	    // http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
+	    // To avoid running into nondeterministic failures to get the process output
+	    // if there's no waiting for the threads, call join() on each Thread (StreamGobbler) object.
+	    // From Thread API: join() "Waits for this thread (the thread join() is invoked on) to die."
+	    outputGobbler.join(); 
+	    errorGobbler.join();
+	    inputGobbler.join(); 
+	    
+
+	    // set the variables the code that created a SafeProcess object may want to inspect
+	    this.outputStr = outputGobbler.getOutput();
+	    this.errorStr = errorGobbler.getOutput();
+	    
+	    // Since we didn't have an exception, process should have terminated now (waitFor blocks until then)
+	    // Set process to null so we don't forcibly terminate it below with process.destroy()
+	    prcs = null;
+
+	    return this.exitValue;
+    }
+
+
+    // Run a very basic process: with no reading from or writing to the Process' iostreams,
+    // this just execs the process and waits for it to return
+    public int runBasicProcess() {
+	Process prcs = null;
+	try {
+	    // 1. create the process
+	    prcs = doRuntimeExec();
+	    // 2. basic waitFor the process to finish
+	    this.exitValue = prcs.waitFor();
+
+	    
+	} catch(IOException ioe) {
+	    if(exceptionHandler != null) {
+		exceptionHandler.gotException(ioe);
+	    } else {
+		//logger.error("IOException: " + ioe.getMessage(), ioe);
+		System.err.println("IOException " + ioe.getMessage());
+		ioe.printStackTrace();
+	    }
+	} catch(InterruptedException ie) {
+
+	    if(exceptionHandler != null) {
+		exceptionHandler.gotException(ie);
+	    } else {
+		//logger.error("Process InterruptedException: " + ie.getMessage(), ie);
+		System.err.println("Process InterruptedException " + ie.getMessage());
+		///ie.printStackTrace(); // an interrupt here is not an error, it can be a cancel action
+	    }
+	    
+	    Thread.currentThread().interrupt();
+	} finally { 
+
+	    if( prcs != null ) {
+		prcs.destroy(); // see runProcess() below
+	    }
+	}
+	return this.exitValue;
+    }
+
+    // Runs a process with default stream processing. Returns the exitValue
+    public int runProcess() {
+	return runProcess(null, null, null); // use default processing of all 3 of the process' iostreams
+    }
+
+    // Run a process with custom stream processing (any custom handlers passed in that are null
+    // will use the default stream processing). 
+    // Returns the exitValue from running the Process
+    public int runProcess(CustomProcessHandler procInHandler,
+			   CustomProcessHandler procOutHandler,
+			   CustomProcessHandler procErrHandler)
+    {
+	Process prcs = null;
+	SafeProcess.OutputStreamGobbler inputGobbler = null;
+	SafeProcess.InputStreamGobbler errorGobbler = null;
+	SafeProcess.InputStreamGobbler outputGobbler = null;
+
+	try {
+	    // 1. get the Process object
+	    prcs = doRuntimeExec();
+	    
+
+	    // 2. create the streamgobblers and set any specified handlers on them
+
+	    // PROC INPUT STREAM
+	    if(procInHandler == null) {
+		// send inputStr to process. The following constructor can handle inputStr being null
+		inputGobbler = // WriterToProcessInputStream
+		    new SafeProcess.OutputStreamGobbler(prcs.getOutputStream(), this.inputStr);
+	    } else { // user will do custom handling of process' InputStream 
+		inputGobbler = new SafeProcess.OutputStreamGobbler(prcs.getOutputStream(), procInHandler);
+	    }
+
+	    // PROC ERR STREAM to monitor for any error messages or expected output in the process' stderr
+	    if(procErrHandler == null) {
+		errorGobbler // ReaderFromProcessOutputStream
+		    = new SafeProcess.InputStreamGobbler(prcs.getErrorStream(), splitStdErrorNewLines);
+	    } else {
+		errorGobbler
+		    = new SafeProcess.InputStreamGobbler(prcs.getErrorStream(), procErrHandler);
+	    }
+
+            // PROC OUT STREAM to monitor for the expected std output line(s)
+	    if(procOutHandler == null) {
+		outputGobbler
+		    = new SafeProcess.InputStreamGobbler(prcs.getInputStream(), splitStdOutputNewLines);
+	    } else {
+		outputGobbler
+		    = new SafeProcess.InputStreamGobbler(prcs.getInputStream(), procOutHandler);
+	    }
+
+
+            // 3. kick off the stream gobblers
+	    this.exitValue = waitForWithStreams(prcs, inputGobbler, outputGobbler, errorGobbler);
+       
+	} catch(IOException ioe) {
+	    if(exceptionHandler != null) {
+		exceptionHandler.gotException(ioe);
+	    } else {
+		//logger.error("IOexception: " + ioe.getMessage(), ioe);
+		System.err.println("IOexception " + ioe.getMessage());
+		ioe.printStackTrace();
+	    }
+	} catch(InterruptedException ie) {
+
+	    if(exceptionHandler != null) {
+		exceptionHandler.gotException(ie);
+	    } else {
+		//logger.error("Process InterruptedException: " + ie.getMessage(), ie);
+		System.err.println("Process InterruptedException " + ie.getMessage());
+		///ie.printStackTrace(); // an interrupt here is not an error, it can be a cancel action
+	    }
+
+	    // propagate interrupts to worker threads here?
+	    // unless the interrupt emanated from any of them in any join()...
+	    // Only if the thread SafeProcess runs in was interrupted
+	    // do we propagate the interrupt to the worker threads.
+	    // http://stackoverflow.com/questions/2126997/who-is-calling-the-java-thread-interrupt-method-if-im-not
+	    // "I know that in JCiP it is mentioned that you should never interrupt threads you do not own"
+	    // But SafeProcess owns the worker threads, so it have every right to interrupt them
+	    // Also read http://stackoverflow.com/questions/13623445/future-cancel-method-is-not-working?noredirect=1&lq=1
+	    if(Thread.currentThread().isInterrupted()) {
+		inputGobbler.interrupt();
+		errorGobbler.interrupt();
+		outputGobbler.interrupt();	    
+	    }
+
+	    // On catchingInterruptedException, re-interrupt the thread.
+	    // This is just how InterruptedExceptions tend to be handled
+	    // See also http://stackoverflow.com/questions/4906799/why-invoke-thread-currentthread-interrupt-when-catch-any-interruptexception
+	    // and https://praveer09.github.io/technology/2015/12/06/understanding-thread-interruption-in-java/
+
+	    // http://stackoverflow.com/questions/3976344/handling-interruptedexception-in-java
+	    // http://stackoverflow.com/questions/4906799/why-invoke-thread-currentthread-interrupt-when-catch-any-interruptexception
+	    // "Only code that implements a thread's interruption policy may swallow an interruption request. General-purpose task and library code should never swallow interruption requests."
+	    // Does that mean that since this code implements this thread's interruption policy, it's ok
+	    // to swallow the interrupt this time and not let it propagate by commenting out the next line?
+	    Thread.currentThread().interrupt(); // re-interrupt the thread - which thread? Infinite loop?
+	} finally { 
+
+	    // Moved into here from GS2PerlConstructor which said
+	    // "I need to somehow kill the child process. Unfortunately Thread.stop() and Process.destroy() both fail to do this. But now, thankx to the magic of Michaels 'close the stream suggestion', it works fine."
+	    // http://steveliles.github.io/invoking_processes_from_java.html
+	    // http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
+	    // http://mark.koli.ch/leaky-pipes-remember-to-close-your-streams-when-using-javas-runtimegetruntimeexec	    
+	    if( prcs != null ) {		
+		prcs.destroy();
+	    }
+	}
+	
+	return this.exitValue;
+    }
+    
+    public int runProcess(LineByLineHandler outLineByLineHandler, LineByLineHandler errLineByLineHandler)
+    {
+	Process prcs = null;
+	SafeProcess.OutputStreamGobbler inputGobbler = null;
+	SafeProcess.InputStreamGobbler errorGobbler = null;
+	SafeProcess.InputStreamGobbler outputGobbler = null;
+
+	try {
+	    // 1. get the Process object
+	    prcs = doRuntimeExec();
+	    
+
+	    // 2. create the streamgobblers and set any specified handlers on them
+
+	    // PROC INPUT STREAM
+	    // send inputStr to process. The following constructor can handle inputStr being null
+	    inputGobbler = // WriterToProcessInputStream
+		new SafeProcess.OutputStreamGobbler(prcs.getOutputStream(), this.inputStr);
+
+	    // PROC ERR STREAM to monitor for any error messages or expected output in the process' stderr    
+	    errorGobbler // ReaderFromProcessOutputStream
+		    = new SafeProcess.InputStreamGobbler(prcs.getErrorStream(), splitStdErrorNewLines);		
+            // PROC OUT STREAM to monitor for the expected std output line(s)
+	    outputGobbler
+		= new SafeProcess.InputStreamGobbler(prcs.getInputStream(), splitStdOutputNewLines);
+
+
+	    // 3. register line by line handlers, if any were set, for the process stderr and stdout streams
+	    if(outLineByLineHandler != null) {
+		outputGobbler.setLineByLineHandler(outLineByLineHandler);
+	    }
+	    if(errLineByLineHandler != null) {
+		errorGobbler.setLineByLineHandler(errLineByLineHandler);
+	    }	    
+
+
+            // 4. kick off the stream gobblers
+	    this.exitValue = waitForWithStreams(prcs, inputGobbler, outputGobbler, errorGobbler);
+       
+	} catch(IOException ioe) {
+	    if(exceptionHandler != null) {
+		exceptionHandler.gotException(ioe);
+	    } else {
+		//logger.error("IOexception: " + ioe.getMessage(), ioe);
+		System.err.println("IOexception " + ioe.getMessage());
+		ioe.printStackTrace();
+	    }
+	} catch(InterruptedException ie) {
+
+	    if(exceptionHandler != null) {
+		exceptionHandler.gotException(ie);
+	    } else {
+		//logger.error("Process InterruptedException: " + ie.getMessage(), ie);
+		System.err.println("Process InterruptedException " + ie.getMessage());
+		///ie.printStackTrace(); // an interrupt here is not an error, it can be a cancel action
+	    }
+
+	    // propagate interrupts to worker threads here?
+	    // unless the interrupt emanated from any of them in any join()...
+	    // Only if the thread SafeProcess runs in was interrupted
+	    // do we propagate the interrupt to the worker threads.
+	    // http://stackoverflow.com/questions/2126997/who-is-calling-the-java-thread-interrupt-method-if-im-not
+	    // "I know that in JCiP it is mentioned that you should never interrupt threads you do not own"
+	    // But SafeProcess owns the worker threads, so it have every right to interrupt them
+	    // Also read http://stackoverflow.com/questions/13623445/future-cancel-method-is-not-working?noredirect=1&lq=1
+	    if(Thread.currentThread().isInterrupted()) {
+		inputGobbler.interrupt();
+		errorGobbler.interrupt();
+		outputGobbler.interrupt();	    
+	    }
+
+	    // On catchingInterruptedException, re-interrupt the thread.
+	    // This is just how InterruptedExceptions tend to be handled
+	    // See also http://stackoverflow.com/questions/4906799/why-invoke-thread-currentthread-interrupt-when-catch-any-interruptexception
+	    // and https://praveer09.github.io/technology/2015/12/06/understanding-thread-interruption-in-java/
+
+	    // http://stackoverflow.com/questions/3976344/handling-interruptedexception-in-java
+	    // http://stackoverflow.com/questions/4906799/why-invoke-thread-currentthread-interrupt-when-catch-any-interruptexception
+	    // "Only code that implements a thread's interruption policy may swallow an interruption request. General-purpose task and library code should never swallow interruption requests."
+	    // Does that mean that since this code implements this thread's interruption policy, it's ok
+	    // to swallow the interrupt this time and not let it propagate by commenting out the next line?
+	    Thread.currentThread().interrupt(); // re-interrupt the thread - which thread? Infinite loop?
+	} finally { 
+
+	    // Moved into here from GS2PerlConstructor which said
+	    // "I need to somehow kill the child process. Unfortunately Thread.stop() and Process.destroy() both fail to do this. But now, thankx to the magic of Michaels 'close the stream suggestion', it works fine."
+	    // http://steveliles.github.io/invoking_processes_from_java.html
+	    // http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
+	    // http://mark.koli.ch/leaky-pipes-remember-to-close-your-streams-when-using-javas-runtimegetruntimeexec	    
+	    if( prcs != null ) {		
+		prcs.destroy();
+	    }
+	}
+	
+	return this.exitValue;
+    }
+
+
+//******************** Inner class and interface definitions ********************//
+// Static inner classes can be instantiated without having to instantiate an object of the outer class first
+
+// Can have public static interfaces too,
+// see http://stackoverflow.com/questions/71625/why-would-a-static-nested-interface-be-used-in-java
+// Implementors need to take care that the implementations are thread safe
+// http://stackoverflow.com/questions/14520814/why-synchronized-method-is-not-included-in-interface
+public static interface ExceptionHandler {
+
+    // when implementing ExceptionHandler.gotException(), if it manipulates anything that's
+    // not threadsafe, declare gotException() as a synchronized method to ensure thread safety
+    public void gotException(Exception e); // can't declare as synchronized in interface method declaration
+}
+
+// Write your own run() body for any StreamGobbler. You need to create an instance of a class
+// extending CustomProcessHandler for EACH IOSTREAM of the process that you want to handle.
+// Do not create a single CustomProcessHandler instance and reuse it for all three streams,
+// i.e. don't call SafeProcess' runProcess(x, x, x); It should be runProcess(x, y, z).
+// Make sure your implementation is threadsafe if you're sharing immutable objects between the threaded streams
+// example implementation is in the GS2PerlConstructor.SynchronizedProcessHandler class.
+// CustomProcessHandler is made an abstract class instead of an interface to force classes that want
+// to use a CustomProcessHandler to create a separate class that extends CustomProcessHandler, rather than
+// that the classes that wish to use it "implementing" the CustomProcessHandler interface itself: the
+// CustomProcessHandler.run() method may then be called in the major thread from which the Process is being
+// executed, rather than from the individual threads that deal with each iostream of the Process.
+public static abstract class CustomProcessHandler {
+
+    protected final int source;
+
+    protected CustomProcessHandler(int src) {
+	this.source = src; // STDERR or STDOUT or STDIN
+    }
+    
+    public abstract void run(Closeable stream); //InputStream or OutputStream
+}
+
+// When using the default stream processing to read from a process' stdout or stderr stream, you can
+// create a class extending LineByLineHandler for the process' err stream and one for its output stream
+// to do something on a line by line basis, such as sending the line to a log
+public static abstract class LineByLineHandler {
+    protected final int source;
+
+    protected LineByLineHandler(int src) {
+	this.source = src; // STDERR or STDOUT
+    }
+
+    public abstract void gotLine(String line); // first non-null line
+    public abstract void gotException(Exception e); // for when an exception occurs instead of getting a line
+}
+
+
+//**************** StreamGobbler Inner class definitions (stream gobblers copied from GLI) **********//
+
+// http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
+// This class is used in FormatConversionDialog to properly read from the stdout and stderr
+// streams of a Process, Process.getInputStream() and Process.getErrorSream()
+public static class InputStreamGobbler extends Thread
+{
+    private InputStream is = null;
+    private StringBuffer outputstr = new StringBuffer();
+    private boolean split_newlines = false;
+    private CustomProcessHandler customHandler = null;
+    private LineByLineHandler lineByLineHandler = null;
+
+    public InputStreamGobbler(InputStream is)
+    {
+	this.is = is;
+	this.split_newlines = false;
+    }
+    
+    public InputStreamGobbler(InputStream is, boolean split_newlines)
+    {
+	this.is = is;
+	this.split_newlines = split_newlines;
+    }
+    
+    public InputStreamGobbler(InputStream is, CustomProcessHandler customHandler)
+    {
+	this.is = is;
+	this.customHandler = customHandler;
+    }
+
+    public void setLineByLineHandler(LineByLineHandler lblHandler) {
+	this.lineByLineHandler = lblHandler;
+    }
+
+    // default run() behaviour
+    public void runDefault()
+    {
+	BufferedReader br = null;
+	try {
+	    br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+	    String line=null;
+	    while ( (line = br.readLine()) != null ) {
+
+		if(this.isInterrupted()) { // should we not instead check if SafeProcess thread was interrupted?
+		    //logger.info("Got interrupted when reading lines from process err/out stream.");
+		    //System.err.println("InputStreamGobbler.runDefault() Got interrupted when reading lines from process err/out stream.");
+		    break; // will go to finally block
+		}
+
+		//System.out.println("@@@ GOT LINE: " + line);
+		outputstr.append(line);
+		if(split_newlines) {
+		    outputstr.append(Utility.NEWLINE); // "\n" is system dependent (Win must be "\r\n")
+		}
+
+		if(lineByLineHandler != null) { // let handler deal with newlines
+		    lineByLineHandler.gotLine(line);
+		}
+	    }
+	} catch (IOException ioe) {
+	    if(lineByLineHandler != null) {
+		lineByLineHandler.gotException(ioe);
+	    } else {
+		//logger.error("Exception when reading from a process' stdout/stderr stream: ", ioe);
+		System.err.println("Exception when reading from a process' stdout/stderr stream: ");
+		ioe.printStackTrace();  
+	    }
+	} finally {
+	    SafeProcess.closeResource(br);
+	}
+    }
+    
+    public void runCustom() {
+	this.customHandler.run(is);
+    }
+    
+    public void run() {
+	if(this.customHandler == null) {
+	    runDefault();
+	} else {
+	    runCustom();
+	}
+    }
+
+    public String getOutput() { 
+	return outputstr.toString(); // implicit toString() call anyway. //return outputstr; 
+    }
+} // end static inner class InnerStreamGobbler
+
+
+// http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
+// This class is used in FormatConversionDialog to properly write to the inputstream of a Process
+// Process.getOutputStream()
+public static class OutputStreamGobbler extends Thread
+{
+    private OutputStream os = null;
+    private String inputstr = "";
+    private CustomProcessHandler customHandler = null;
+
+    public OutputStreamGobbler(OutputStream os) {
+	this.os = os;
+    }
+
+    public OutputStreamGobbler(OutputStream os, String inputstr)
+    {
+	this.os = os;
+	this.inputstr = inputstr;
+    }
+
+    public OutputStreamGobbler(OutputStream os, CustomProcessHandler customHandler) {
+	this.os = os;
+	this.customHandler = customHandler;
+    }
+
+    // default run() behaviour
+    public void runDefault() {
+	
+	if (inputstr == null) {
+		return;
+	}
+	
+	BufferedWriter osw = null;
+	try {
+	    osw = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
+	    //System.out.println("@@@ SENDING LINE: " + inputstr);
+	    osw.write(inputstr, 0, inputstr.length());
+	    osw.newLine();//osw.write("\n");
+	    osw.flush();
+	    
+	    // Don't explicitly send EOF when using StreamGobblers as below, 
+	    // as the EOF char is echoed to output.
+	    // Flushing the write handle and/or closing the resource seems 
+	    // to already send EOF silently.
+	    
+	    /*if(Utility.isWindows()) {
+	      osw.write("\032"); // octal for Ctrl-Z, EOF on Windows
+	      } else { // EOF on Linux/Mac is Ctrl-D
+	      osw.write("\004"); // octal for Ctrl-D, see http://www.unix-manuals.com/refs/misc/ascii-table.html
+	      }
+	      osw.flush();
+	    */
+	} catch (IOException ioe) {
+	    //logger.error("Exception writing to SafeProcess' inputstream: ", ioe);
+	    System.err.println("Exception writing to SafeProcess' inputstream: ");
+	    ioe.printStackTrace();	
+	} finally {
+	    SafeProcess.closeResource(osw);
+	}
+    }
+
+    // call the user's custom handler for the run() method
+    public void runCustom() {
+	this.customHandler.run(os);	
+    }
+
+    public void run()
+    {
+	if(this.customHandler == null) {
+	    runDefault();
+	} else {
+	    runCustom();
+	}
+
+    }	
+} // end static inner class OutputStreamGobbler
+
+
+//**************** Useful static methods. Copied from GLI's Utility.java ******************
+    // For safely closing streams/handles/resources. 
+    // For examples of use look in the Input- and OutputStreamGobbler classes.
+    // http://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html
+    // http://stackoverflow.com/questions/481446/throws-exception-in-finally-blocks
+    public static boolean closeResource(Closeable resourceHandle) {
+	boolean success = false;
+	try {
+	    if(resourceHandle != null) {
+		resourceHandle.close();
+		resourceHandle = null;
+		success = true;
+	    }
+	} catch(Exception e) {
+	    //logger.error("Exception closing resource: ", e);
+	    System.err.println("Exception closing resource: " + e.getMessage());
+	    e.printStackTrace();
+	    resourceHandle = null;
+	    success = false;
+	} finally {
+	    return success;
+	}
+    }
+
+    public static boolean closeProcess(Process prcs) {
+	boolean success = true;
+	if( prcs != null ) { 
+	    success = success && closeResource(prcs.getErrorStream()); 
+	    success = success && closeResource(prcs.getOutputStream()); 
+	    success = success && closeResource(prcs.getInputStream()); 
+	    prcs.destroy(); 
+	}
+	return success;
+    }
+
+} // end class SafeProcess
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/StaticStrings.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/StaticStrings.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/StaticStrings.java	(revision 31635)
@@ -0,0 +1,296 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+
+
+/** Were you to guess that this is a class object choc-a-block full of static stringy goodness, you'd be right. They come in several flavours: Those ending _STR are strings you might find as values in XML, _ELEMENT are element names, _ATTRIBUTE are attribute names, _CHAR are particular characters while _CHARACTER are also characters but expressed as strings (for regex purposes ie startsWith, endsWith, indexOf and replaceAll); _PATTERN are strings which require extra escaping to put them through regex. Finally anything else is just a static string used within GLI. */
+public class StaticStrings {
+    static final public char CLOSEBRACKET_CHAR = ']';
+    static final public char DOUBLEQUOTE_CHAR = '\"';
+    static final public char BACKSLASH_CHAR = '\\';
+    static final public char FORWARDSLASH_CHAR = '/';
+    static final public char NEW_LINE_CHAR = '\n';
+    static final public char NS_SEP = '.';
+    static final public char OPENBRACKET_CHAR = '[';
+    static final public char SINGLEQUOTE_CHAR = '\'';
+    static final public char SPACE_CHAR = ' ';
+    static final public char STAR_CHAR = '*';
+    static final public String ACCENTFOLD_OPTION_STR = "accentfold";
+    static final public String ALLFIELDS_STR = "allfields";
+    static final public String ARCPLUG_STR = "ArchivesInfPlugin";
+    static final public String ASSIGNED_ATTRIBUTE = "assigned";
+    static final public String ASSOCIATIONS_ELEMENT = "Associations";
+    static final public String ATTRIBUTE_ELEMENT = "Attribute";
+    static final public String BROWSE_STR = "browse";
+    static final public String BUILDTYPE_ELEMENT = "BuildType";
+    static final public String BUILDTYPE_STR = "buildtype";
+    static final public String CASEFOLD_OPTION_STR = "casefold";
+    static final public String CCS_STR = "ccs";
+    static final public String CLASSIFIER_STR = "classifier";
+    static final public String CLASSIFY_ELEMENT = "Classify";
+    static final public String CLASSIFY_STR = "classify";
+    static final public String CLOSE_PARENTHESIS_CHARACTER = ")";
+    static final public String CODE_ATTRIBUTE = "code";
+    static final public String COLNAME_PATTERN = "<col_name>";
+    static final public String COLLECT_CFG = "collect.cfg";
+    static final public String COLLECTION_CFG_XML = "collectionConfig.xml";
+    static final public String COLLECTDIR_ARGUMENT = "-collectdir";
+    static final public String COLLECTION_ELEMENT = "Collection";
+    static final public String COLLECTION_STR = "Collection: ";
+    static final public String COLLECTIONMETADATA_COLLECTIONNAME_STR = "collectionname";
+    static final public String COLLECTIONMETADATA_COLLECTIONEXTRA_STR = "collectionextra";
+    static final public String COLLECTIONMETADATA_CREATOR_ELEMENT = "Creator";
+    static final public String COLLECTIONMETADATA_CREATOR_STR = "creator";
+    static final public String COLLECTIONMETADATA_ELEMENT = "CollectionMetadata";
+    static final public String COLLECTIONMETADATA_ICONCOLLECTION_STR = "iconcollection";
+    static final public String COLLECTIONMETADATA_ICONCOLLECTIONSMALL_STR = "iconcollectionsmall";
+    static final public String COLLECTIONMETADATA_MAINTAINER_ELEMENT = "Maintainer";
+    static final public String COLLECTIONMETADATA_MAINTAINER_STR = "maintainer";
+    static final public String COLLECTIONMETADATA_PUBLIC_ELEMENT = "Public";
+    static final public String COLLECTIONMETADATA_PUBLIC_STR = "public";
+    static final public String COLLECTIONMETADATA_COLLECTGROUP_STR = "collectgroup";
+    static final public String COLLECTIONMETADATA_STR = "collectionmeta";
+    static final public String COLON_CHARACTER = ":";
+    static final public String COMMA_CHARACTER = ",";
+    static final public String COMMENTED_INDEXES_STR = "#indexes";
+    static final public String COMMENTED_INDEX_DEFAULT_STR = "#defaultindex";
+    static final public String COMMENTED_LEVELS_STR = "#levels";
+    static final public String COMMENTED_LEVEL_DEFAULT_STR = "#defaultlevel";
+    static final public String COMMENTED_INDEXOPTIONS_STR = "#indexoptions";
+    static final public String CONTENT_ATTRIBUTE = "content";
+    static final public String CONTENT_ELEMENT = "content";
+    static final public String DATABASETYPE_ELEMENT = "infodbtype";
+    static final public String DATABASETYPE_STR = "infodbtype";
+    static final public String DATELIST_CLASSIFIER = "DateList";
+    static final public String DEBUG_ARGUMENT = "-debug";
+    static final public String DESCRIPTION_ELEMENT = "Description";
+    static final public String DESCRIPTION_STR = "description";
+    static final public String DICTIONARY_ATTRIBUTE = "dictionary";
+    static final public String DIRECTORY_MAPPINGS_ELEMENT = "DirectoryMappings";
+    static final public String DISPLAYITEMLIST_STR = "displayItemList";
+    static final public String DISPLAY_STR = "display";
+    static final public String DISPLAYITEM_STR = "displayItem";
+    static final public String DISPLAYITEM_ELEMENT = "DisplayItem"; // Note: 1st letter is titlecase
+    static final public String DOCUMENT_STR = "document";
+    static final public String DOT_CHARACTER = ".";
+    static final public String EMBEDMETAPLUG_STR = "EmbeddedMetadataPlugin";
+    static final public String EMPTY_STR = "";
+    static final public String ENGLISH_LANGUAGE_STR = "en";
+    static final public String ENTRY_ELEMENT = "Entry";
+    static final public String ENUM_STR = "enum";
+    static final public String ENUM_STRING_STR = "enumstring";
+    static final public String EQUALS_CHARACTER = "=";
+    static final public String EXCLAMATION_CHARACTER = "!";
+    static final public String EXCLUDE_STR = "exclude";
+    static final public String EXTENSION_ATTRIBUTE = "extension";
+    static final public String EXTRACTED_NAMESPACE = "ex.";
+    static final public String FACET_ELEMENT = "Facet";
+    static final public String FACET_STR = "Facet";
+    static final public String FACET_LOW_STR = "facet";
+    static final public String FALSE_STR = "false";
+    static final public String FEEDBACK_ARGUMENT = "-feedback";
+    static final public String FILE_ATTRIBUTE = "file";
+    static final public String FILENAME_STR = "Filename";
+    static final public String FILTER_ATTRIBUTE = "filter";
+    static final public String FLAG_STR = "flag";
+    static final public String FORMAT_ELEMENT = "Format";
+    static final public String FORMAT_STR = "format";
+    static final public String FORMAT_START_TAG = "<format>";
+    static final public String FORMAT_END_TAG = "</format>";
+    static final public String FURTHER_DIALOG_INDICATOR = "...";
+    static final public String FEDORA_MODE= "-fedora";
+    static final public String FEDORA_HOME = "-fedora_home";
+    static final public String FEDORA_VERSION = "-fedora_version";
+    static final public String FEDORA_HOSTNAME = "-fedora_hostname";
+    static final public String FEDORA_PORT     = "-fedora_port";
+    static final public String FEDORA_USERNAME = "-fedora_username";
+    static final public String FEDORA_PASSWORD = "-fedora_password";
+    static final public String FEDORA_PROTOCOL = "-fedora_protocol";
+    static final public String GLI_ATTRIBUTE = "gli";
+    static final public String GLISERVER_URL_ARGUMENT = "-gliserver_url";
+  static final public String GLOBAL_STR = "global";
+    static final public String GREATER_THAN_CHARACTER = ">";
+    static final public String GSDL_ARGUMENT = "-gsdl";
+    static final public String GSDL3_ARGUMENT = "-gsdl3";
+    static final public String GSDL3_SRC_ARGUMENT = "-gsdl3src";
+    static final public String GSDLOS_ARGUMENT = "-gsdlos";
+    static final public String HFILE_ARGUMENT = "-hfile";
+    static final public String HIERARCHY_CLASSIFIER = "Hierarchy";
+    static final public String HIERARCHY_STR = "hierarchy";
+    static final public String HELP_ARGUMENT = "-help";
+    static final public String HTM_FILE_EXTENSION = ".htm";
+    static final public String ICON_STR = "icon";
+    static final public String ID_STR = "id";
+    static final public String IMAGES_PATH_RELATIVE_TO_GSDL_PREFIX = "_httpprefix_/collect/<col_name>/images/";
+    static final public String INCLUDE_STR = "include";
+    static final public String INDEX_ELEMENT = "Index";
+    static final public String INDEX_LOW_STR = "index";
+    static final public String INDEX_DEFAULT_ELEMENT = "DefaultIndex";
+    static final public String INDEX_DEFAULT_ELEMENT_LOWERCASE = "defaultIndex";
+    static final public String INDEX_DEFAULT_STR = "defaultindex";
+    static final public String INDEX_STR = "indexes";
+    static final public String INDEXES_ELEMENT = "Indexes";
+    static final public String INDEXOPTION_ELEMENT = "Option";
+    static final public String INDEXOPTIONS_ELEMENT = "IndexOption";
+    static final public String INDEXOPTION_STR = "indexOption";
+    static final public String INDEXOPTIONS_STR = "indexoptions";
+    static final public String INDEXOPTION_DEFAULT_ELEMENT = "DefaultIndexOption";
+    static final public String INFODB_STR = "infodb";
+    static final public String INT_STR = "int";
+    static final public String IMPORT_STR = "import";
+  static final public String IMPORT_OPTION_STR = "importOption";
+  static final public String BUILD_OPTION_STR = "buildOption";
+  
+    static final public String[] KEEP_PLUG = { "GreenstoneXMLPlugin", "GreenstoneMETSPlugin", "EmbeddedMetadataPlugin" };
+    static final public String KEY_ATTRIBUTE = "key";
+    static final public String LANGUAGE_ARGUMENT = "l=";
+    static final public String LANGUAGE_ATTRIBUTE = "language";
+    static final public String LANGUAGE_ELEMENT = "Language";
+    static final public String LANGUAGE_DEFAULT_ELEMENT = "DefaultLanguage";
+    static final public String LANGUAGE_DEFAULT_INDEX_ELEMENT = "defaultIndexLanguage";
+    static final public String LANGUAGE_DEFAULT_STR = "defaultlanguage";
+    static final public String LANGUAGE_INDEX_ELEMENT = "indexLanguage";
+    static final public String LANGUAGE_METADATA_ELEMENT = "LanguageMetadata";
+    static final public String LANGUAGE_METADATA_ELEMENT_STR = "languageMetadata";
+    static final public String LANGUAGE_METADATA_STR = "languagemetadata";
+    static final public String LANG_STR = "lang";
+    static final public String LANG_ATTRIBUTE = "lang";
+    static final public String LANGUAGE_STR = "language";
+    static final public String LANGUAGES_ELEMENT = "Languages";
+    static final public String LANGUAGES_STR = "languages";
+    static final public String LBRACKET_CHARACTER = "[";
+    static final public String LESS_THAN_CHARACTER = "<";
+    static final public String LEVEL_ATTRIBUTE = "level";
+    static final public String LEVEL_ELEMENT = "level";
+    static final public String LEVEL_DEFAULT_ELEMENT = "defaultLevel";
+    static final public String LEVEL_DEFAULT_STR = "defaultlevel";
+    static final public String LEVELS_STR = "levels";
+    static final public String LIBRARY_URL_ARGUMENT = "-library_url";
+	static final public String LINUX_OPEN_COMMAND = "xdg-open %1";
+    static final public String LOCAL_LIBRARY_ARGUMENT = "-local_library";
+    static final public String LOAD_ARGUMENT = "-load";
+    static final public String MAC_OPEN_COMMAND = "open %1";
+    static final public String MAPPING_ELEMENT = "Mapping";
+    static final public String MDS_ATTRIBUTE = "mds";
+    static final public String METADATA_ARGUMENT = "-metadata";
+    static final public String METADATA_ELEMENT = "Metadata";
+    static final public String METADATA_SET_EXTENSION = ".mds";
+    static final public String METADATA_STR = "metadata";
+    static final public String METADATALIST_STR = "metadataList";
+    static final public String METADATALIST_ELEMENT = "MetadataList";
+    static final public String METADATA_TYPE_STR = "metadata";
+    static final public String METADATA_XML = "metadata.xml";
+    static final public String METADATAXMLPLUG_STR = "MetadataXMLPlugin";
+    static final public String MGPP_ATTRIBUTE = "mgpp_enabled";
+    static final public String MINUS_CHARACTER = "-";
+    static final public String MODE_STR = "Mode: ";
+    static final public String MODEL_COLLECTION_NAME = "modelcol";
+    static final public String NAME_ATTRIBUTE = "name";
+    static final public String NAME_STR = "name";
+    static final public String NAME_ELEMENT = "Name";
+    static final public String NEWLINE_CHARACTER = "\\";
+    static final public String NO_LOAD_ARGUMENT = "-no_load";
+    static final public String ONE_CHARACTER = "1";
+    static final public String OPEN_PARENTHESIS_CHARACTER = "(";
+    static final public String OPTION_ELEMENT = "Option";
+    static final public String OPTION_STR = "option";
+
+    static final public String OPTIONS_ATTRIBUTE = "options";
+    static final public String PARAGRAPH_STR = "paragraph";
+    static final public String PERL_ARGUMENT = "-perl";
+    static final public String PHIND_CLASSIFIER = "Phind";
+    static final public String PLUGIN_ELEMENT = "Plugin";
+    static final public String PLUGIN_STR = "plugin";
+    static final public String PLUGINLIST_STR = "pluginList";
+    static final public String PREDEFINED_METADATA_ATTRIBUTE = "predefined";          
+    static final public String RBRACKET_CHARACTER = "]";
+    static final public String RECPLUG_STR = "DirectoryPlugin";
+    static final public String REGEXP_STR = "regexp";
+  static final public String REPLACELIST_STR = "replaceList";
+    static final public String REPLACELISTREF_STR = "replaceListRef";
+    static final public String RESTRICTED_METADATA_ATTRIBUTE = "restricted";
+    static final public String SEARCHTYPE_ELEMENT = "searchType";
+    static final public String SEARCHTYPE_STR = "searchtype";
+    static final public String SEARCH_STR = "search";
+  static final public String SECURITY_STR = "security";
+    static final public String SECTION_ELEMENT = "Section";
+    static final public String SECTION_STR = "section";
+    static final public String SEPARATE_CJK_OPTION_STR = "separate_cjk";
+    static final public String SEPARATOR_ATTRIBUTE = "separator";
+    static final public String SEPARATOR_CHARACTER = "/";
+	static final public String SERVICE_RACK_LIST_ELEMENT = "serviceRackList";
+    static final public String SERVLET_ARGUMENT ="-servlet";
+    static final public String SINGLE_QUOTE_CHARACTER = "'";
+    static final public String SITE_ARGUMENT = "-site";
+    static final public String SMALLICON_STR = "smallicon";
+    static final public String SOLR_ELEMENT = "Solr";
+    static final public String SORT_ELEMENT = "Sort";
+    static final public String SORT_STR = "Sort";
+    static final public String SORT_LOW_STR = "sort";
+  static final public String SORT_DEFAULT_ELEMENT = "defaultSort";
+    static final public String SPACE_CHARACTER = " ";
+    static final public String SPECIAL_ATTRIBUTE = "special";
+    static final public String SPEECH_CHARACTER = "\"";
+    static final public String STEM_OPTION_STR = "stem";
+    static final public String STOP_CHARACTER = ".";
+    static final public String SUBCOLLECTION_ELEMENT = "Subcollection";
+    static final public String SUBCOLLECTION_STR = "subcollection";
+    static final public String SUBCOLLECTION_DEFAULT_INDEX_ELEMENT = "DefaultSubcollection";
+    static final public String SUBCOLLECTION_DEFAULT_INDEX_STR = "defaultsubcollection";
+    static final public String SUBCOLLECTION_INDEX_STR = "indexsubcollections";
+    static final public String SUBCOLLECTION_INDEX_ELEMENT = "indexSubcollection";
+    static final public String SUBCOLLECTION_INDEXES_ELEMENT = "SubcollectionIndexes";
+    static final public String SUPERCOLLECTION_ELEMENT = "Supercollection";
+    static final public String SUPERCOLLECTION_STR = "supercollection";
+    static final public String TAB_CHARACTER = "\t";
+    static final public String TEXT_NODE = "#text";
+    static final public String TEXT_STR = "text";
+    static final public String TIMESTAMP_ARGUMENT = "&uq=";
+    static final public String TITLE_ELEMENT = "Title";
+    static final public String SOURCE_ELEMENT = "Source";
+    static final public String TRUE_STR = "true";
+    static final public String TYPE_ATTRIBUTE = "type";
+    static final public String UNKNOWN_ELEMENT = "Unknown";
+    static final public String UNKNOWNPLUG_STR = "UnknownPlugin";
+    static final public String USE_METADATA_FILES_ARGUMENT = "use_metadata_files";
+    static final public String USE_REMOTE_GREENSTONE_ARGUMENT = "-use_remote_greenstone";
+    static final public String URL_SEPARATOR_CHARACTER = "/";
+    static final public String VALUE_ATTRIBUTE = "value";
+    static final public String VERSION_ATTRIBUTE = "version";
+    static final public String WIN_9X_OPEN_COMMAND = "command.com /c start \"%1\"";
+    static final public String WIN_OPEN_COMMAND = "cmd.exe /c start \"\" \"%1\"";
+    static final public String YES_STR = "yes";
+    static final public String ZERO_CHARACTER = "0";
+    static final public String METADATA_PATH                              = "-metadata_path";
+    static final public String NEW_METADATASET                            = "-new_set";
+    static final public String CACHE_FOLDER                               = "cache";
+    static final public String CUSTOM_ATTRIBUTE                           = "custom";
+    static final public String MG_STR                           = "mg";
+    static final public String MGPP_STR                           = "mgpp";
+    static final public String LUCENE_STR                           = "lucene";
+    
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/SynchronizedTreeModelTools.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/SynchronizedTreeModelTools.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/SynchronizedTreeModelTools.java	(revision 31635)
@@ -0,0 +1,137 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+package org.greenstone.gatherer.util;
+
+import java.lang.Runnable;
+import javax.swing.SwingUtilities;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.metadata.FilenameEncoding;
+import org.greenstone.gatherer.collection.CollectionTreeNode;
+
+/** Due to the TreeModel objects not having any synchronization, certain assumptions, such as the model state remaining constant during a repaint, don't always hold - especially given that I'm changing the tree model on a different thread. In order to get around this I will use the latest swing paradigm wherein you flag a section of code to be executed by the AWT GUI Event queue, as soon as other gui tasks have finished. This way I shouldn't have tree redraws throwing NPEs because the array size of the children of a certain node has changed -while- the repaint call was made, i.e. repaint() calls getChildCount() = 13, removeNodeFromParent() called, repaint calls getChildAt(12) = ArrayIndexOutOfBoundsException. 
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3c
+ */
+public class SynchronizedTreeModelTools {
+    /** Adds an insertNodeInto model update onto the AWT Event queue. This gets around the lack of synchronization illustrated above. */
+    static final public Runnable insertNodeInto(DefaultTreeModel model, MutableTreeNode parent, MutableTreeNode target_node) {
+	return insertNodeInto(model, parent, target_node, true);
+    }
+
+    static final public Runnable insertNodeInto(final DefaultTreeModel model, final MutableTreeNode parent, final MutableTreeNode target_node, final boolean wait_allowed) {
+	final Runnable doInsertNodeInto = new Runnable() {
+		public void run() {
+		    ///ystem.err.print("Running task... ");
+		    DebugStream.println("insertNodeInto(" + model + ", " + parent + ", " + target_node + ", " + wait_allowed);
+		    int index = -1;
+		    int pos = 0;
+		    while(index == -1 && pos < parent.getChildCount()) {
+			TreeNode node = parent.getChildAt(pos);
+			int result = 0;
+			///ystem.err.println("Compare " + target_node + " to " + node);
+			if((target_node.isLeaf() && node.isLeaf()) || (!target_node.isLeaf() && !node.isLeaf())) {
+			    result = target_node.toString().toLowerCase().compareTo(node.toString().toLowerCase());
+			}
+			else if(target_node.isLeaf()) {
+			    result = 1;
+			}
+			else {
+			    result = -1;
+			}
+			if(result > 0) {
+			    ///ystem.err.println("Keep searching...");
+			    pos++;
+			}
+			else {
+			    ///ystem.err.println("Found!");
+			    index = pos;
+			}
+		    }
+		    if(index == -1) {
+			index = parent.getChildCount();
+		    }
+		    model.insertNodeInto(target_node, parent, index);
+		}
+	    };
+	///ystem.err.print("Queuing Task... ");
+	try {
+	    if(wait_allowed && !SwingUtilities.isEventDispatchThread()) {
+		///ystem.err.print("In another thread - invoke and wait... ");
+		SwingUtilities.invokeAndWait(doInsertNodeInto);
+	    }
+	    else {
+		///ystem.err.print("In Event Thread or wait not allowed - invoke later... ");
+		SwingUtilities.invokeLater(doInsertNodeInto);
+	    }
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	///ystem.err.print("Added Task... ");
+	return doInsertNodeInto;
+    }
+
+
+    /** Adds a removeNodeFromParent model update onto the AWT Event queue. This gets around the lack of synchronization illustrated above.
+     * @param model The <strong>GTreeModel</strong> we want to remove the node from.
+     * @param target_node The <strong>GTreeNode</strong> to remove.
+     */
+    static final public void removeNodeFromParent(final DefaultTreeModel model, final MutableTreeNode target_node) {
+	///ystem.err.println("Remove " + target_node + " from parent in model " + model);
+	final Runnable doRemoveNodeFromParent = new Runnable() {
+		public void run() {
+			// If we're dealing with a collection tree node, it may have 
+			// gs.FilenameEncoding assigned, so we remove its entry from the map. 
+			// Needs to be done here because the tree is constantly changing 
+			// when nodes are being removed, renamed and deleted, and this 
+			// affects lookup queries sent to the map. 
+			// Don't need to do a recursive reset on this coltreenode, because 
+			// Delete/Move/Rename FileJobs were created for *each* node			
+			if(target_node instanceof CollectionTreeNode) {
+				CollectionTreeNode colNode = (CollectionTreeNode)target_node;
+				FilenameEncoding.map.remove(colNode.getURLEncodedFilePath());
+			}
+			
+		    model.removeNodeFromParent(target_node);			
+		}
+	    };
+	try {
+	    //SwingUtilities.invokeLater(doRemoveNodeFromParent);
+	    SwingUtilities.invokeAndWait(doRemoveNodeFromParent);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+	// If we've thrown an error because we tried to invoke the runnable task and wait, when we are in the AWTEvent thread already, then try agin but with an invoke later.
+	catch (java.lang.Error error) {
+	    if(error.toString().equals("java.lang.Error: Cannot call invokeAndWait from the event dispatcher thread")) {
+		SwingUtilities.invokeLater(doRemoveNodeFromParent);
+	    }
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/TableUtils.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/TableUtils.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/TableUtils.java	(revision 31635)
@@ -0,0 +1,92 @@
+package org.greenstone.gatherer.util;
+
+import java.awt.*;
+import javax.swing.*;
+import javax.swing.table.*;
+
+/**
+ * Utilites for manipulating JTables.
+ *
+ * @author Marc Hedlund
+ *   <a href="mailto:marc@precipice.org">&lt;marc@precipice.org&gt;</a>
+ * @version $Revision$
+ */
+
+public class TableUtils  {
+
+  // This is based on code found at
+  // <http://java.sun.com/docs/books/tutorial/ui/swingComponents/table.html>,
+  // somewhat modified.
+
+//    public static void setPreferredCellSizes(JTable table) {
+//      TableModel model = table.getModel();
+//      TableColumnModel colModel = table.getColumnModel();
+
+//      for (int i = 0; i < model.getColumnCount(); i++) {
+//        TableColumn column = colModel.getColumn(i);
+
+//        int longestCell = 0;
+//        int highestCell = 0;
+
+//        for (int j = 0; j < model.getRowCount(); j++) {
+//          Object value = model.getValueAt(j, i);
+//          if (value == null) continue;
+
+//          Component cell =
+//            table.getDefaultRenderer(model.getColumnClass(i)).
+//            getTableCellRendererComponent(table, value,
+//                                          false, false, j, i);
+
+//          int width = cell.getPreferredSize().width;
+//          int height = cell.getPreferredSize().height;
+
+//          if (width > longestCell) longestCell = width;         
+//          if (height > highestCell) highestCell = height;
+//        }
+
+//        Component headerComp = column.getHeaderRenderer().
+//          getTableCellRendererComponent(table, column.getHeaderValue(),
+//                                        false, false, 0, 0);
+
+//        int headerWidth = headerComp.getPreferredSize().width;
+//        int headerHeight = headerComp.getPreferredSize().height;
+
+//        column.setPreferredWidth(Math.max(headerWidth, longestCell));
+
+//        int currentHeight = table.getRowHeight();
+//        int preferredHeight = Math.max(headerHeight, highestCell);
+
+//        table.setRowHeight(Math.max(currentHeight, preferredHeight));
+//      }
+//    }                                                                   
+
+  /**
+   * Takes a column in an existing table and makes it fixed-width.
+   * Specifically, it sets the column's minimum and maximum widths to
+   * its preferred width, and disables auto-resize for the table as a
+   * whole.
+   *
+   * <p>
+   *
+   * Later on this should take a column array for efficiency.
+   *
+   * @param table JTable The table to modify
+   * @param colIndex int Which column to fix
+   * @return int The width of the column as it was fixed
+   */
+
+  public static int fixColumnToPreferredWidth(JTable table, int colIndex) {
+    table.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN);
+
+    TableColumnModel tcm = table.getColumnModel();
+    TableColumn col = tcm.getColumn(colIndex);
+    int width = col.getPreferredWidth();
+
+    col.setMaxWidth(width);
+    col.setMinWidth(width);    
+
+    //table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+
+    return width;
+  }
+}  
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/UnzipTools.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/UnzipTools.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/UnzipTools.java	(revision 31635)
@@ -0,0 +1,98 @@
+/**
+ *############################################################################
+ * A component of the Greenstone Librarian Interface, part of the Greenstone
+ * digital library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * Author: David Bainbridge, NZDL Project, University of Waikato, NZ
+ *
+ * Copyright (C) 2005 New Zealand Digital Library Project
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *############################################################################
+ */
+
+package org.greenstone.gatherer.util;
+
+
+import java.io.*;
+import java.util.*;
+import java.util.zip.*;
+
+
+public class UnzipTools
+{
+    static public void unzipFile(String zip_file_path, String base_directory_path)
+    {
+	try {
+	    ZipFile zip_file = new ZipFile(new File(zip_file_path), ZipFile.OPEN_READ);
+	    
+	    Enumeration e = zip_file.entries(); 
+	    while (e.hasMoreElements()) {
+		ZipEntry zip_entry = (ZipEntry) e.nextElement();
+		File zip_entry_file = new File(base_directory_path + zip_entry.getName());
+		// System.err.println("    Unzipping: " + zip_entry_file.getAbsolutePath());
+
+		// Directory case
+		if (zip_entry.isDirectory()) {
+		    // Create named directory, if it doesn't already exist
+		    if (!zip_entry_file.exists() && !zip_entry_file.mkdirs()) {
+			System.err.println("Error: unable to create directory " + zip_entry_file);
+		    }
+		}
+
+		// File case
+		else {
+		    // Write out file to disk
+
+		    // Make sure its parent directory exists.
+		    File dir = new File(zip_entry_file.getParent());
+		    dir.mkdirs();
+
+		    // Don't need to unzip files called empty which were created during the zipping process
+		    // in order to be able to generate empty directories in a zip (see remote.ZipTools)
+		    if(!zip_entry_file.getName().equals("empty")) {
+			// Set up input stream
+			InputStream zis = zip_file.getInputStream(zip_entry); 
+			BufferedInputStream bzis = new BufferedInputStream(zis); 
+			DataInputStream dbzis = new DataInputStream(bzis);
+			
+			// Set up output stream
+			FileOutputStream fzos = new FileOutputStream(zip_entry_file);
+			BufferedOutputStream bfzos = new BufferedOutputStream(fzos);
+			
+			byte[] buf = new byte[1024];
+			int len;
+			while ((len = dbzis.read(buf)) >= 0) {
+			    bfzos.write(buf,0,len);
+			}
+			
+			dbzis.close();
+			bzis.close();
+			zis.close();
+			
+			bfzos.close();
+			fzos.close();
+		    }
+		}
+	    }
+
+	    zip_file.close();
+	} 
+	catch (Exception exception) {
+	    exception.printStackTrace();
+	}
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/Utility.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/Utility.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/Utility.java	(revision 31635)
@@ -0,0 +1,603 @@
+/**
+ *#########################################################################
+ *
+ * A component of the Gatherer application, part of the Greenstone digital
+ * library suite from the New Zealand Digital Library Project at the
+ * University of Waikato, New Zealand.
+ *
+ * <BR><BR>
+ *
+ * Author: John Thompson, Greenstone Digital Library, University of Waikato
+ *
+ * <BR><BR>
+ *
+ * Copyright (C) 1999 New Zealand Digital Library Project
+ *
+ * <BR><BR>
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * <BR><BR>
+ *
+ * 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.
+ *
+ * <BR><BR>
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *########################################################################
+ */
+
+package org.greenstone.gatherer.util;
+
+
+// Don't even think about adding import java.awt.* here!
+// The functions in this class should not use any graphical classes. Put your function somewhere else buster!
+import java.io.*;
+import java.net.*;
+import java.util.*;
+// Don't even think about adding import javax.swing.* here!
+// The functions in this class should not use any graphical classes. Put your function somewhere else buster!
+import org.greenstone.gatherer.Dictionary;
+// Don't even think about adding import org.greenstone.gatherer.Gatherer in here!
+// The functions in this class should be independent of the Gatherer class. Put your function somewhere else buster!
+import org.greenstone.gatherer.util.SafeProcess; // for the closeResource() static method
+
+/** To provide a library of common methods, in a static context, for use in the Gatherer.
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3b
+ */
+public class Utility
+{
+    /** Definition of an important directory name, in this case the file the collection configuration is expect to be in. */
+    static final public String CONFIG_FILE = "etc" + File.separator + "collect.cfg";
+    static final public String CONFIG_GS3_FILE = "etc" + File.separator + "collectionConfig.xml";
+    static final public String COLLECT_CFG = "collect.cfg";
+    static final public String COLLECT_BAK = "collect.bak";
+    static final public String COLLECTION_CONFIG_XML = "collectionConfig.xml";
+    static final public String COLLECTION_CONFIG_BAK = "collectionConfig.bak";
+    static final public String GS3MODE_ARGUMENT = "-gs3mode";
+    static final public String BUILD_CFG = "build.cfg";
+    static final public String BUILD_CONFIG_XML = "buildConfig.xml";
+
+    /** The default name of the perl executable under unix. */
+    static final public String PERL_EXECUTABLE_UNIX = "perl";
+    /** The default name of the perl executable under windows. */
+    static final public String PERL_EXECUTABLE_WINDOWS = "Perl.exe";
+
+    /** Platform independent NEWLINE character */
+    public static final String NEWLINE;    
+    
+    // NEWLINE related code copied across from GS3 src code
+    // Before Java 7, no System.lineSeparator() or System.getProperty("line.separator")
+    // And on local linux, am compiling with JDK 6, so need this.
+    // http://stackoverflow.com/questions/207947/how-do-i-get-a-platform-dependent-new-line-character
+    // http://stackoverflow.com/questions/2591083/getting-java-version-at-runtime
+    // https://www.tutorialspoint.com/java/lang/package_getspecificationversion.htm
+    // https://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html
+    // Can initialise static final vars on declaration or in static initialisation code block
+    // http://stackoverflow.com/questions/2339932/java-can-final-variables-be-initialized-in-static-initialization-block
+    // Initialise object member final vars on declaration or in constructors
+    static {	
+	double java_version = Double.parseDouble(System.getProperty("java.specification.version"));
+	if(java_version >= 1.7) {
+	    NEWLINE = System.getProperty("line.separator");
+	} else {
+	    NEWLINE = isWindows() ? "\r\n" : "\n";
+	}	
+    }
+
+    /** 
+     * Reads in a text file and returns the contents as a String
+     */
+    static public String readFile(File file) {
+	BufferedReader fin = null;
+	StringBuffer contents = new StringBuffer();
+	
+	try {
+	    fin = new BufferedReader(new FileReader(file));
+	    String line = null;
+	    while((line = fin.readLine()) != null) {
+		contents.append(line);
+		contents.append("\n");
+	    }
+	} catch(IOException e) {
+	    System.err.println("*** Could not read in file: " + file.toString());
+	    System.err.println("*** Exception occurred: " + e.getMessage());
+	} finally {
+	    SafeProcess.closeResource(fin);	    
+	}
+
+	return contents.toString();
+    }
+
+    /**
+     * Delete a file or directory
+     * @param file The <strong>File</strong> you want to delete.
+     * @return A <i>boolean</i> which is <i>true</i> if the file specified was successfully deleted, <i>false</i> otherwise.
+     */
+    static public boolean delete(File file)
+    {
+	// Nothing to do if it doesn't exist
+	if (!file.exists()) {
+	    return true;
+	}
+
+	return deleteInternal(file);
+    }
+
+
+    /** Convenience function. */
+    static public boolean delete(String filename)
+    {
+	return delete(new File(filename));
+    }
+
+
+    /** In Java you have to make sure a directory is empty before you delete it, so recursively delete. */
+    static private boolean deleteInternal(File file)
+    {
+	// If file is a directory, we have to recursively delete its contents first
+	if (file.isDirectory()) {
+	    File files[] = file.listFiles();
+	    for (int i = 0; i < files.length; i++) {
+		if (deleteInternal(files[i]) == false) {
+		    System.err.println("Error: Could not delete folder " + file);
+		    return false;
+		}
+	    }
+	}
+
+	// Delete file
+	if (file.delete() == false) {
+	    System.err.println("Error: Could not delete file " + file);
+	    return false;
+	}
+
+	return true;
+    }
+
+
+    /** Convert a long, detailing the length of a file in bytes, into a nice human readable string using b, kb, Mb and Gb. */
+    static final public String BYTE_SUFFIX = " b";
+    static final public long GIGABYTE = 1024000000l;
+    static final public String GIGABYTE_SUFFIX = " Gb";
+    static final public long KILOBYTE =       1024l;
+    static final public String KILOBYTE_SUFFIX = " kb";
+    static final public long MEGABYTE =    1024000l;
+    static final public String MEGABYTE_SUFFIX = " Mb";
+    static final public String formatFileLength(long length) {
+	StringBuffer result = new StringBuffer("");
+	float number = 0f;
+	String suffix = null;
+	// Determine the floating point number and the suffix (radix) used.
+	if(length >= GIGABYTE) {
+	    number = (float) length / (float) GIGABYTE;
+	    suffix = GIGABYTE_SUFFIX;
+	}
+	else if(length >= MEGABYTE) {
+	    number = (float) length / (float) MEGABYTE;
+	    suffix = MEGABYTE_SUFFIX;
+	}
+	else if(length >= KILOBYTE) {
+	    number = (float) length / (float) KILOBYTE;
+	    suffix = KILOBYTE_SUFFIX;
+	}
+	else {
+	    // Don't need to do anything fancy if the file is smaller than a kilobyte
+	    return length + BYTE_SUFFIX;
+	}
+	// Create the formatted string remembering to round the number to 2.d.p. To do this copy everything in the number string from the start to the first occurance of '.' then copy two more digits. Finally search for and print anything that appears after (and including) the optional 'E' delimter.
+	String number_str = Float.toString(number);
+	char number_char[] = number_str.toCharArray();
+	int pos = 0;
+	// Print the characters up to the '.'
+	while(number_char != null && pos < number_char.length && number_char[pos] != '.') {
+	    result.append(number_char[pos]);
+	    pos++;
+	}
+	if(pos < number_char.length) {
+	    // Print the '.' and at most two characters after it
+	    result.append(number_char[pos]);
+	    pos++;
+	    for(int i = 0; i < 2 && pos < number_char.length; i++, pos++) {
+		result.append(number_char[pos]);
+	    }
+	    // Search through the remaining string for 'E'
+	    while(pos < number_char.length && number_char[pos] != 'E') {
+		pos++;
+	    }
+	    // If we still have string then we found an E. Copy the remaining string.
+	    while(pos < number_char.length) {
+		result.append(number_char[pos]);
+		pos++;
+	    }
+	}
+	// Add suffix
+	result.append(suffix);
+	// Done
+	return result.toString();
+    }
+
+    /** This method formats a given string, using HTML markup, so its width does not exceed the given width and its appearance if justified.
+     * @param text The <strong>String</strong> requiring formatting.
+     * @param width The maximum width per line as an <i>int</i>.
+     * @return A <strong>String</strong> formatted so as to have no line longer than the specified width.
+     * TODO Currently HTML formatting tags are simply removed from the text, as the effects of spreading HTML tags over a break are undetermined. To solve this we need to associate tags with a certain text token so if it gets broken on to the next line the tags go with it, or if the tags cover a sequence of words that are broken we need to close then reopen the tags. However all this is a major task and well beyond anything I have time to 'muck-round' on.
+     */
+    static public String formatHTMLWidth(String text, int width) {
+	if(text == null) {
+	    return "Error";
+	}
+	HTMLStringTokenizer html = new HTMLStringTokenizer(text);
+	int current_width = 0;
+	int threshold = width / 2;
+	Stack lines = new Stack();
+	String line = "";
+	while(html.hasMoreTokens()) {
+	    String token = html.nextToken();
+	    while(token != null) {
+		if(html.isTag()) {
+		    // Insert smart HTML tag code here.
+		    token = null;
+		}
+		else {
+		    // If the token is bigger than two thirds width, before we've even started break it down.
+		    if(current_width + 1 + token.length() > width && token.length() > threshold) {
+			if(width == current_width) {
+			    lines.push(line);
+			    line = token;
+			    current_width = token.length();
+			}
+			else {
+			    String prefix = token.substring(0, width - 1 - current_width);
+			    token = token.substring(prefix.length());
+			    if(current_width == 0) {
+				line = line + prefix;
+			    }
+			    else {
+				line = line + " " + prefix;
+			    }
+			    lines.push(line);
+			    line = "";
+			    current_width = 0;
+			}
+		    }
+		    // If adding the next token would push us over the maximum line width.
+		    else if(current_width + 1 + token.length() > width) {
+			lines.push(line);
+			line = token;
+			current_width = token.length();
+			token = null;
+		    }
+		    // Otherwise we should be able to just add the token, give or take.
+		    else {
+			if(current_width == 0) {
+			    line = line + token;
+			    current_width = token.length();
+			}
+			else {
+			    // Special case for standard punctuation which may exist after a tag like so:
+			    // My name is <scratchy>Slim Shady</scratchy>.   <-- Annoying punctuation.
+			    if(token.equals(".") || token.equals(",") || token.equals("!") || token.equals("?")) {
+				line = line + token;
+				current_width = current_width + 1;
+			    }
+			    else {
+				line = line + " " + token;
+				current_width = current_width + 1 + token.length();
+			    }
+			}
+			token = null;
+		    }
+		}
+	    }
+	}
+	String result = line;
+	while(!lines.empty()) {
+	    result = (String)lines.pop() + "<BR>" + result;
+	}
+	// Replace ' ' with "&nbsp;"
+	boolean tag = false;
+	int pos = 0;
+	while(pos < result.length()) {
+	    if(result.charAt(pos) == '<') {
+		tag = true;
+	    }
+	    else if(result.charAt(pos) == '>') {
+		tag = false;
+	    }
+	    else if(result.charAt(pos) == ' ' && !tag) {
+		String prefix = result.substring(0, pos);
+		String suffix = result.substring(pos + 1);
+		result = prefix + "&nbsp;" + suffix;
+	    }
+	    pos++;
+	}
+	result = "<HTML>" + result + "</HTML>";
+	return result;
+    }
+
+
+    static public String getDateString() {
+	Calendar current = Calendar.getInstance();
+	String day_name = null;
+	switch(current.get(Calendar.DAY_OF_WEEK)) {
+	case Calendar.MONDAY: day_name = "Dates.Mon"; break;
+	case Calendar.TUESDAY: day_name = "Dates.Tue"; break;
+	case Calendar.WEDNESDAY: day_name = "Dates.Wed"; break;
+	case Calendar.THURSDAY: day_name = "Dates.Thu"; break;
+	case Calendar.FRIDAY: day_name = "Dates.Fri"; break;
+	case Calendar.SATURDAY: day_name = "Dates.Sat"; break;
+	case Calendar.SUNDAY: day_name = "Dates.Sun"; break;
+	default: day_name = "";
+	}
+	String month_name = null;
+	switch(current.get(Calendar.MONTH)) {
+	case Calendar.JANUARY: month_name = "Dates.Jan"; break;
+	case Calendar.FEBRUARY: month_name = "Dates.Feb"; break;
+	case Calendar.MARCH: month_name = "Dates.Mar"; break;
+	case Calendar.APRIL: month_name = "Dates.Apr"; break;
+	case Calendar.MAY: month_name = "Dates.May"; break;
+	case Calendar.JUNE: month_name = "Dates.Jun"; break;
+	case Calendar.JULY: month_name = "Dates.Jul"; break;
+	case Calendar.AUGUST: month_name = "Dates.Aug"; break;
+	case Calendar.SEPTEMBER: month_name = "Dates.Sep"; break;
+	case Calendar.OCTOBER: month_name = "Dates.Oct"; break;
+	case Calendar.NOVEMBER: month_name = "Dates.Nov"; break;
+	case Calendar.DECEMBER: month_name = "Dates.Dec"; break;
+	default: month_name = "";
+	}
+	int day = current.get(Calendar.DAY_OF_MONTH);
+	int hour = current.get(Calendar.HOUR_OF_DAY);
+	int minute = current.get(Calendar.MINUTE);
+	int second = current.get(Calendar.SECOND);
+	int year = current.get(Calendar.YEAR);
+
+	return Dictionary.get(day_name) + " " + Dictionary.get(month_name) + " " + day + " " + year + " " + Utility.pad(String.valueOf(hour), 2, '0', true) + ":" + Utility.pad(String.valueOf(minute), 2, '0', true) + ":" + Utility.pad(String.valueOf(second), 2, '0', true);
+    }
+
+
+    /** Determine this machines name.
+     * @return The name as a <strong>String</strong>.
+     */
+    static public String getMachineName() {
+	try {
+	    return InetAddress.getLocalHost().getHostName();
+	}
+	catch(UnknownHostException ex) {
+	}
+	return "Unknown Machine";
+    }
+
+
+    static public String getSitesDir(String gsdl3_path) {
+	return gsdl3_path + "sites" + File.separator;
+
+    }
+
+    /** @return the OSdir foldername: windows, linux or darwin */
+    public static String getOSdirName() {
+	if(Utility.isWindows()) {
+	    return "windows";
+	}	
+	if(Utility.isMac()) {
+	    return "darwin";
+	}
+	return "linux"; // else assume it's a linux machine, OSdirname = linux
+    }
+    
+
+    /** Method to determine if the host system is MacOS based.
+     * @return a boolean which is true if the platform is MacOS, false otherwise
+     */
+    public static boolean isMac() {
+	Properties props = System.getProperties();
+	String os_name = props.getProperty("os.name","");
+	if(os_name.startsWith("Mac OS")) {
+	    return true;
+	}
+	return false;
+    }
+
+   
+    /** Method to determine if the host system is Microsoft Windows based.
+     * @return A <i>boolean</i> which is <i>true</i> if the platform is Windows, <i>false</i> otherwise.
+     */
+    public static boolean isWindows() {
+	Properties props = System.getProperties();
+	String os_name = props.getProperty("os.name","");
+	if(os_name.startsWith("Windows")) {
+	    return true;
+	}
+	return false;
+    }
+
+    public static boolean isWindows9x() {
+	Properties props = System.getProperties();
+	String os_name = props.getProperty("os.name","");
+	if(os_name.startsWith("Windows") && os_name.indexOf("9") != -1) {
+	    return true;
+	}
+	return false;
+    }
+    /** Takes a string and a desired length and pads out the string to the length by adding spaces to the left.
+     * @param str The target <strong>String</strong> that needs to be padded.
+     * @param length The desired length of the string as an <i>int</i>.
+     * @return A <strong>String</strong> made from appending space characters with the string until it has a length equal to length.
+     */
+    static private String pad(String str_raw, int length, char fill, boolean end) {
+	StringBuffer str = new StringBuffer(str_raw);
+	while(str.length() < length) {
+	    if(end) {
+		str.insert(0, fill);
+	    }
+	    else {
+		str.append(fill);
+	    }
+	}
+	return str.toString();
+    }
+
+
+    /** Builds the cache dir by appending the user path and 'cache'.
+     * @return a File representing the path to the private file cache within the current collection.
+     */
+    public static File getCacheDir() {
+	return new File(getGLIUserFolder(), StaticStrings.CACHE_FOLDER);
+    }
+
+   /** Method which constructs the log directory given a certain collection.
+     * @param col_dir The location of the collection directory as a <strong>String</strong>.
+     * @return The location of the given collections log directory, also as a <strong>String</strong>.
+     */
+    public static String getLogDir(String col_dir) {
+	if(col_dir != null) {
+	    return col_dir + LOG_DIR;
+	}
+	else {
+	    return getGLIUserFolder().getAbsolutePath() + File.separator + LOG_DIR;
+	}
+    }
+
+    static final private String APPLICATION_DATA_FOLDER = "Application Data";
+    static final private String UNIX_GLI_CONFIG_FOLDER = ".gli";
+    static final private String USER_HOME_PROPERTY = "user.home";
+    static final private String WIN_GLI_CONFIG_FOLDER = "Greenstone" + File.separator + "GLI";
+     /** Definition of an important directory name, in this case the log directory for the collection. */
+    static final public String LOG_DIR = "log" + File.separator;
+
+    static public File getGLIUserFolder()
+    {
+	if (Utility.isWindows()) {
+	    return new File(System.getProperty(USER_HOME_PROPERTY) + File.separator + APPLICATION_DATA_FOLDER + File.separator + WIN_GLI_CONFIG_FOLDER + File.separator);
+	}
+	else {
+	    return new File(System.getProperty(USER_HOME_PROPERTY) + File.separator + UNIX_GLI_CONFIG_FOLDER + File.separator);
+	}
+    }
+
+  static private HashMap plugin_map = null;
+  
+  static private void setUpPluginNameMap() {
+    plugin_map = new HashMap();
+    plugin_map.put("GAPlug", "GreenstoneXMLPlugin");
+    plugin_map.put("RecPlug", "DirectoryPlugin");
+    plugin_map.put("ArcPlug","ArchivesInfPlugin");
+    plugin_map.put("TEXTPlug","TextPlugin");
+    plugin_map.put("XMLPlug","ReadXMLFile");
+    plugin_map.put("EMAILPlug","EmailPlugin");
+    plugin_map.put("SRCPlug","SourceCodePlugin");
+    plugin_map.put("NULPlug","NulPlugin");
+    plugin_map.put("W3ImgPlug","HTMLImagePlugin");
+    plugin_map.put("PagedImgPlug","PagedImagePlugin");
+    plugin_map.put("METSPlug", "GreenstoneMETSPlugin");
+    plugin_map.put("DBPlug", "DatabasePlugin");
+    plugin_map.put("PPTPlug", "PowerPointPlugin");
+    plugin_map.put("PSPlug", "PostScriptPlugin");
+  }
+
+  static public String ensureNewPluginName(String plugin) {
+    if (plugin.endsWith("Plugin")) return plugin;
+    if (plugin_map == null) {
+      setUpPluginNameMap();
+    }
+    String new_name = (String)plugin_map.get(plugin);
+    if (new_name != null) return new_name;
+    new_name = plugin.replaceAll("Plug", "Plugin");
+    return new_name;
+  }
+
+    
+  /** Write out a property line--a (property, value) pair--to the gsdl(3)site.cfg file.
+     * If the file already contains the line as-is, it is not re-written.	
+     * If the file doesn't contain the line, it is appended.
+     * If the file contained a different value for the property, the line is corrected
+     * and the file is written out.
+     * If the propertyValue parameter is null, the property line is removed from the file.
+     * Not using the Properties class, as we want to keep the file's contents in the 
+     * same order and preserve all the comments in as they're meant to help the user.
+     * Return the old value for the property, if it existed, else "".	 
+     */
+    public static String updatePropertyConfigFile(
+		String filename, String propertyName, String propertyValue) 
+	{	
+	File propFile = new File(filename);
+	String oldValue = "";
+	if(!propFile.exists()) {
+		System.err.println("*** Unable to update property " + propertyName + " in file " 
+				+ filename + " to\n" + propertyValue + ". File does not (yet) exist.\n");
+		return oldValue;
+	}	
+	BufferedReader fin = null;
+	BufferedWriter fout = null;
+	StringBuffer contents = new StringBuffer();
+	String insertLine = null;
+	if(propertyValue != null) {
+		insertLine = propertyName+"\t"+propertyValue+"\n"; // new line after every propertyLine
+	}
+	boolean found = false;
+	try {
+	    fin = new BufferedReader(new FileReader(filename));
+	    String line = "";
+	    while((line = fin.readLine()) != null) {
+			line = line.trim(); // remove any preceding (surrounding) whitespace
+			if(line.startsWith(propertyName)) { // won't match comment
+			    found = true;
+				// store the previous value for the property
+				oldValue = line;
+				oldValue = oldValue.substring(propertyName.length());
+				oldValue = oldValue.trim();
+				
+				if(propertyValue != null) { // line should be removed if propertyValue == null
+				    if(line.equals(insertLine)) { // file is already correct, nothing to do
+						fin.close();
+						fin = null;
+						break;
+				    } else {
+						contents.append(insertLine);
+				    }
+				}
+			} else { // any other line
+			    contents.append(line);		    
+			    contents.append("\n"); // ensures the required new line at end of file
+			}
+	    }		
+
+	    if(fin != null) { // need to write something out to the file
+			fin.close();
+			fin = null;
+
+			// if collecthome/property wasn't already specified in the file, append it
+			// but only if we have a value to write out to the file
+			if(!found && propertyValue != null) {		
+				fout = new BufferedWriter(new FileWriter(filename, true)); // append mode
+				fout.write(insertLine, 0, insertLine.length());
+			} else {		    
+				fout = new BufferedWriter(new FileWriter(filename)); // hopefully this will overwrite
+				fout.write(contents.toString(), 0, contents.length());
+			}
+
+			fout.close();
+			fout = null;
+			
+	    } // else the file is fine
+	} catch(IOException e) {
+	    System.err.println("*** Could not update file: " + filename); 
+	    System.err.println("with the " + propertyName + " property set to " + propertyValue);
+	    System.err.println("Exception occurred: " + e.getMessage());
+	} finally {
+	    SafeProcess.closeResource(fin);
+	    SafeProcess.closeResource(fout);	    
+	    
+	}
+	return oldValue;
+    }
+}
Index: /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/XMLTools.java
===================================================================
--- /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/XMLTools.java	(revision 31635)
+++ /main/tags/GS3-src-SafeProcess/gli-src/org/greenstone/gatherer/util/XMLTools.java	(revision 31635)
@@ -0,0 +1,1269 @@
+package org.greenstone.gatherer.util;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import org.apache.xerces.parsers.*;
+import org.apache.xml.serialize.*;
+import org.greenstone.gatherer.DebugStream;
+import org.w3c.dom.*;
+import org.xml.sax.*;
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter; // for elementToString()
+
+// SAX
+import org.xml.sax.XMLReader;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.InputSource;
+
+// JAXP
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.FactoryConfigurationError;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+// for elementToString():
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+
+/** This class is a static class containing useful XML functions */
+public class XMLTools
+{
+	/** extracts the text out of a node */
+	public static Node getNodeTextNode(Element param)
+	{
+		param.normalize();
+		Node n = param.getFirstChild();
+		while (n != null && n.getNodeType() != Node.TEXT_NODE)
+		{
+			n = n.getNextSibling();
+		}
+		return n;
+	}
+
+	/** extracts the text out of a node */
+	public static String getNodeText(Element param)
+	{
+		Node text_node = getNodeTextNode(param);
+		if (text_node == null)
+		{
+			return "";
+		}
+		return text_node.getNodeValue();
+	}
+
+	public static void setNodeText(Element elem, String text)
+	{
+		Node old_text_node = getNodeTextNode(elem);
+		if (old_text_node != null)
+		{
+			elem.removeChild(old_text_node);
+		}
+		Text t = elem.getOwnerDocument().createTextNode(text);
+		elem.appendChild(t);
+	}
+
+	/** returns the (first) child element with the given name */
+	public static Node getChildByTagName(Node n, String name)
+	{
+
+		Node child = n.getFirstChild();
+		while (child != null)
+		{
+			if (child.getNodeName().equals(name))
+			{
+				return child;
+			}
+			child = child.getNextSibling();
+		}
+		return null; //not found
+	}
+
+	/**
+	 * returns the (nth) child element with the given name index numbers start
+	 * at 0
+	 */
+	public static Node getChildByTagNameIndexed(Node n, String name, int index)
+	{
+		if (index == -1)
+		{
+			return getChildByTagName(n, name);
+		}
+		int count = 0;
+		Node child = n.getFirstChild();
+		while (child != null)
+		{
+			if (child.getNodeName().equals(name))
+			{
+				if (count == index)
+				{
+					return child;
+				}
+				else
+				{
+					count++;
+				}
+			}
+			child = child.getNextSibling();
+		}
+		return null; //not found
+	}
+
+	/**
+	 * returns the element parent/node_name[@attribute_name='attribute_value']
+	 */
+	public static Element getNamedElement(Element parent, String node_name, String attribute_name, String attribute_value)
+	{
+
+		NodeList children = parent.getChildNodes();
+		for (int i = 0; i < children.getLength(); i++)
+		{
+			Node child = children.item(i);
+			//logger.debug("getnamed elem, node nmae="+child.getNodeName());
+			if (child.getNodeName().equals(node_name))
+			{
+				if (((Element) child).getAttribute(attribute_name).equals(attribute_value))
+					return (Element) child;
+			}
+		}
+		// not found
+		return null;
+	}
+
+	/**
+	 * returns a list of elements
+	 * parent/node_name[@attribute_name='attribute_value']
+	 */
+	public static ArrayList getNamedElementList(Element parent, String node_name, String attribute_name, String attribute_value)
+	{
+		ArrayList elements = new ArrayList();
+		NodeList children = parent.getChildNodes();
+		for (int i = 0; i < children.getLength(); i++)
+		{
+			//System.out.println("getNamedElementList");
+			Node child = children.item(i);
+			//logger.debug("getnamed elem, node nmae="+child.getNodeName());
+			if (child.getNodeName().equals(node_name))
+			{
+				if (((Element) child).getAttribute(attribute_name).equals(attribute_value))
+					elements.add((Element) child);
+			}
+		}
+		// not found
+		if (elements.size() == 0)
+		{
+			elements = null;
+		}
+		return elements;
+	}
+
+	public static void copyAllChildren(Element to, Element from)
+	{
+
+		Document to_doc = to.getOwnerDocument();
+		Node child = from.getFirstChild();
+		while (child != null)
+		{
+			to.appendChild(to_doc.importNode(child, true));
+			child = child.getNextSibling();
+		}
+	}
+
+  /** duplicates all elements in list elements and appends to toElement */
+  public static void duplicateElementList(Document owner, Element toElement, NodeList elements, boolean with_attributes) {
+    int num_elems = elements.getLength();
+    if (num_elems < 1)
+      {
+	return;
+      }
+    for (int i = 0; i < num_elems; i++)
+      {
+	Element to_element = XMLTools.duplicateElement(owner, (Element) elements.item(i), with_attributes);
+	toElement.appendChild(to_element);
+      }
+
+  }
+	/** Duplicates an element */
+	public static Element duplicateElement(Document owner, Element element, boolean with_attributes)
+	{
+		return duplicateElementNS(owner, element, null, with_attributes);
+	}
+
+	/** Duplicates an element */
+	public static Element duplicateElementNS(Document owner, Element element, String namespace_uri, boolean with_attributes)
+	{
+		Element duplicate;
+		if (namespace_uri == null)
+		{
+			duplicate = owner.createElement(element.getTagName());
+		}
+		else
+		{
+			duplicate = owner.createElementNS(namespace_uri, element.getTagName());
+		}
+		// Copy element attributes
+		if (with_attributes)
+		{
+			NamedNodeMap attributes = element.getAttributes();
+			for (int i = 0; i < attributes.getLength(); i++)
+			{
+				Node attribute = attributes.item(i);
+				duplicate.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
+			}
+		}
+
+		// Copy element children
+		NodeList children = element.getChildNodes();
+		for (int i = 0; i < children.getLength(); i++)
+		{
+			Node child = children.item(i);
+			duplicate.appendChild(owner.importNode(child, true));
+		}
+
+		return duplicate;
+	}
+
+	/** Remove all of the child nodes from a certain node. */
+	static final public void clear(Node node)
+	{
+		while (node.hasChildNodes())
+		{
+			node.removeChild(node.getFirstChild());
+		}
+	}
+
+	static public ArrayList getChildElementsByTagName(Element parent_element, String element_name)
+	{
+		ArrayList child_elements = new ArrayList();
+
+		NodeList children_nodelist = parent_element.getChildNodes();
+		for (int i = 0; i < children_nodelist.getLength(); i++)
+		{
+			Node child_node = children_nodelist.item(i);
+			if (child_node.getNodeType() == Node.ELEMENT_NODE && child_node.getNodeName().equals(element_name))
+			{
+				child_elements.add(child_node);
+			}
+		}
+
+		return child_elements;
+	}
+
+	static public String getElementTextValue(Element element)
+	{
+		// Find the first text node child
+		NodeList children_nodelist = element.getChildNodes();
+		for (int i = 0; i < children_nodelist.getLength(); i++)
+		{
+			Node child_node = children_nodelist.item(i);
+			if (child_node.getNodeType() == Node.TEXT_NODE)
+			{
+				return child_node.getNodeValue();
+			}
+		}
+
+		// None found
+		return "";
+	}
+
+	/**
+	 * Method to retrieve the value of a given node.
+	 * 
+	 * @param element
+	 *            The <strong>Element</strong> whose value we wish to find. Soon
+	 *            to be deprecated!
+	 */
+	static final public String getValue(Node element)
+	{
+		if (element == null)
+		{
+			return "";
+		}
+		// If we've been given a subject node first retrieve its value node.
+		if (element.getNodeName().equals("Subject"))
+		{
+			element = getNodeFromNamed(element, "Value");
+		}
+		// If we've got a value node, then reconstruct the text. Remember that DOM will split text over 256 characters into several text nodes
+		if (element != null && element.hasChildNodes())
+		{
+			StringBuffer text_buffer = new StringBuffer();
+			NodeList text_nodes = element.getChildNodes();
+			for (int i = 0; i < text_nodes.getLength(); i++)
+			{
+				Node possible_text = text_nodes.item(i);
+				if (possible_text.getNodeName().equals(StaticStrings.TEXT_NODE))
+				{
+					text_buffer.append(possible_text.getNodeValue());
+				}
+			}
+			return text_buffer.toString();
+		}
+		return "";
+	}
+
+	/**
+	 * Method to retrieve from the node given, a certain child node with the
+	 * specified name.
+	 * 
+	 * @param parent
+	 *            The <strong>Node</strong> whose children should be searched.
+	 * @param name
+	 *            The required nodes name as a <strong>String</strong>.
+	 * @return The requested <strong>Node</strong> if it is found, <i>null</i>
+	 *         otherwise. Soon to be deprecated!
+	 */
+	static final public Node getNodeFromNamed(Node parent, String name)
+	{
+		Node child = null;
+		for (Node i = parent.getFirstChild(); i != null && child == null; i = i.getNextSibling())
+		{
+			if (i.getNodeName().equals(name))
+			{
+				child = i;
+			}
+		}
+		return child;
+	}
+
+	static final public String WELLFORMED = "well-formed !";
+	static final public String NOTWELLFORMED = "not well-formed";
+	static final private String HEADER = "<?xml version='1.0' encoding='UTF-8'?><collectionConfig xmlns:gsf='http://www.greenstone.org/greenstone3/schema/ConfigFormat' xmlns:gslib='http://www.greenstone.org/skinning' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>";
+	static final private String FOOTER = "</collectionConfig>";
+
+
+    public static Document getDOM(String xml_str) 
+    {	
+	Document doc = null;
+	try {
+
+	    DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+	    InputSource is = new InputSource();
+	    is.setCharacterStream(new StringReader(xml_str));
+	    doc = db.parse(is);
+
+	} catch (Exception e) {
+	    e.printStackTrace();
+	}
+	return doc;	
+    }
+
+	public static String parse(String xml_str)
+	{
+		String validation_msg = WELLFORMED;
+		xml_str = HEADER + xml_str + FOOTER;
+		try
+		{
+			SAXParserFactory factory = SAXParserFactory.newInstance();
+			factory.setNamespaceAware(true);
+			//factory.setValidating (true);
+			SAXParser parser = factory.newSAXParser();
+			InputSource iSource = new InputSource(new StringReader(xml_str));
+			//              parser.parse (iSource, new DefaultHandler ());
+
+			org.xml.sax.XMLReader reader = parser.getXMLReader();
+			reader.setContentHandler(new DefaultHandler());
+			reader.setErrorHandler(new DefaultHandler());
+			reader.parse(iSource);
+		}
+		catch (FactoryConfigurationError e)
+		{
+			validation_msg = "unable to get a document builder factory";
+		}
+		catch (ParserConfigurationException e)
+		{
+			validation_msg = "unable to configure parser";
+		}
+		catch (SAXParseException e)
+		{
+			validation_msg = NOTWELLFORMED + getLocationString(e) + e.getMessage();
+		}
+		catch (SAXException e)
+		{
+			validation_msg += " Fatal error: " + e.toString();
+		}
+		catch (IOException e)
+		{
+			validation_msg = "Unable to read the input, i/o error";
+		}
+
+		return validation_msg;
+	}
+
+	//In this method, the parsed string xml_str is not wrapped by the header and footer strings.
+	public static String parseDOM(String xml_str)
+	{
+		String validation_msg = WELLFORMED;
+
+		try
+		{
+			SAXParserFactory factory = SAXParserFactory.newInstance();
+			factory.setNamespaceAware(true);
+			//factory.setValidating (true);
+			SAXParser parser = factory.newSAXParser();
+			InputSource iSource = new InputSource(new StringReader(xml_str));
+			//              parser.parse (iSource, new DefaultHandler ());
+
+			org.xml.sax.XMLReader reader = parser.getXMLReader();
+			reader.setContentHandler(new DefaultHandler());
+			reader.setErrorHandler(new DefaultHandler());
+			reader.parse(iSource);
+		}
+		catch (FactoryConfigurationError e)
+		{
+			validation_msg = "unable to get a document builder factory";
+		}
+		catch (ParserConfigurationException e)
+		{
+			validation_msg = "unable to configure parser";
+		}
+		catch (SAXParseException e)
+		{
+			validation_msg = NOTWELLFORMED + getLocationString(e) + e.getMessage();
+		}
+		catch (SAXException e)
+		{
+			validation_msg += " " + e.toString();
+		}
+		catch (IOException e)
+		{
+			validation_msg = "Unable to read the input, i/o error";
+		}
+
+		return validation_msg;
+	}
+
+	public static String parse(File xml_file)
+	{
+		String validation_msg = WELLFORMED;
+
+		try
+		{
+			SAXParserFactory factory = SAXParserFactory.newInstance();
+			factory.setNamespaceAware(true);
+			//factory.setValidating (true);
+			SAXParser parser = factory.newSAXParser();
+			FileReader r = new FileReader(xml_file);
+			InputSource iSource = new InputSource(r);
+			XMLReader reader = parser.getXMLReader();
+			reader.setContentHandler(new DefaultHandler());
+			reader.setErrorHandler(new DefaultHandler());
+			reader.parse(iSource);
+		}
+		catch (FactoryConfigurationError e)
+		{
+			validation_msg = "unable to get a document builder factory";
+		}
+		catch (ParserConfigurationException e)
+		{
+			validation_msg = "unable to configure parser";
+		}
+		catch (SAXParseException e)
+		{
+			validation_msg = NOTWELLFORMED + getLocationString(e) + e.getMessage();
+		}
+		catch (SAXException e)
+		{
+			validation_msg += " Fatal error: " + e.toString();
+		}
+		catch (IOException e)
+		{
+			validation_msg = "Unable to read the input, i/o error";
+		}
+
+		return validation_msg;
+	}
+
+	/** Returns a string of the location. */
+	private static String getLocationString(SAXParseException ex)
+	{
+		StringBuffer str = new StringBuffer();
+
+		String systemId = ex.getSystemId();
+		if (systemId != null)
+		{
+			int index = systemId.lastIndexOf('/');
+			if (index != -1)
+				systemId = systemId.substring(index + 1);
+			str.append(systemId);
+		}
+		str.append("(line ");
+		str.append(ex.getLineNumber() - 1);
+		str.append(", column ");
+		str.append(ex.getColumnNumber());
+		str.append("): ");
+
+		return str.toString();
+
+	} // getLocationString(SAXParseException):String
+
+	/** Parse an XML document from a given file path */
+	static public Document parseXMLFile(String xml_file_path, boolean use_class_loader)
+	{
+		if (use_class_loader == true)
+		{
+			InputStream is = JarTools.getResourceAsStream("/" + xml_file_path);
+			if (is != null)
+			{
+				return parseXML(is);
+			}
+		}
+
+		// Try the file outside the classes directory
+		return parseXMLFile(new File(xml_file_path));
+	}
+
+	/** Parse an XML document from a given file */
+	static public Document parseXMLFile(File xml_file)
+	{
+		// No file? No point trying!
+		if (xml_file.exists() == false)
+		{
+		    // System.err.println("@@@ file " + xml_file + " does not exist.");
+			return null;
+		}
+
+		try
+		{
+			return parseXML(new FileInputStream(xml_file));
+		}
+		catch (Exception exception)
+		{
+			DebugStream.printStackTrace(exception);
+			return null;
+		}
+	}
+
+	/** Parse an XML document from a given input stream */
+	static public Document parseXML(InputStream xml_input_stream)
+	{
+		Document document = null;
+
+		try
+		{
+			InputStreamReader isr = new InputStreamReader(xml_input_stream, "UTF-8");
+			document = parseXML(isr);
+			isr.close();
+			xml_input_stream.close();
+		}
+		catch (Exception exception)
+		{
+			DebugStream.printStackTrace(exception);
+		}
+
+		return document;
+	}
+
+	/** Parse an XML document from a given reader */
+	static public Document parseXML(Reader xml_reader)
+	{
+		Document document = null;
+
+		// If debugging, the following will store the XML contents to be parsed, 
+		// which can then be inspected upon encountering a SAXException (need to run GLI with -debug on)
+		String xmlContents = "";
+
+		try
+		{
+			Reader reader = null;
+
+			// (1) By default, GLI will remove any contents preceeding (and invalidating) 
+			// the XML and present these lines separately to the user
+			if (!DebugStream.isDebuggingEnabled())
+			{
+				try
+				{
+					reader = new BufferedReader(new RemoveContentBeforeRootElementXMLReader(xml_reader));
+				}
+				catch (Exception e)
+				{
+					System.err.println("Exception while wrapping the reader in parseXML(Reader)");
+					e.printStackTrace();
+				}
+			}
+
+			// (2) If we are running GLI in debug mode:
+			// In case parsing exceptions are thrown (SAX Exceptions), we want to get some
+			// idea of where things went wrong. This will print the "XML" contents to either
+			// system.out (if debugging is off) or to the DebugStream otherwise.
+			// We need to read the XML twice to know the line where things went wrong, so
+			// do the additional reading only if we're debugging 
+			else
+			{
+				StringBuffer buf = new StringBuffer();
+				char[] buffer = new char[500];
+				int numCharsRead = xml_reader.read(buffer, 0, buffer.length);
+				while (numCharsRead != -1)
+				{
+					buf.append(buffer, 0, numCharsRead);
+					numCharsRead = xml_reader.read(buffer, 0, buffer.length);
+				}
+				xmlContents = buf.toString();
+				xml_reader.close(); // closing the old Reader
+				xml_reader = null;
+				buffer = null;
+				buf = null;
+				// we need a Reader to parse the same contents as the Reader that was just closed
+				reader = new BufferedReader(new StringReader(xmlContents));
+				//System.err.println("xmlContents:\n" + xmlContents);
+			}
+
+			// (2) The actual XML parsing
+			InputSource isc = new InputSource(reader);
+			DOMParser parser = new DOMParser();
+			parser.setFeature("http://xml.org/sax/features/validation", false);
+			parser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+			// May or may not be ignored, the documentation for Xerces is contradictory. If it works then parsing -should- be faster.
+			parser.setFeature("http://apache.org/xml/features/dom/defer-node-expansion", true);
+			parser.setFeature("http://apache.org/xml/features/dom/include-ignorable-whitespace", false);
+			parser.setEntityResolver(new GLIEntityResolver());
+			parser.parse(isc);
+			document = parser.getDocument();
+
+		}
+		catch (SAXParseException e)
+		{
+			showXMLParseFailureLine(e, xmlContents);
+		}
+		catch (SAXException exception)
+		{
+			System.err.println("SAX exception: " + exception.getMessage());
+			if (DebugStream.isDebuggingEnabled())
+			{
+				DebugStream.println("Encountered a SAX exception when parsing the following:\n*********START\n" + xmlContents + "\n************END\n");
+				// Exit to let the user view the erroneous line/xml before it goes past the screen buffer?
+				DebugStream.println("Debug mode: Exiting the program as there was trouble parsing the XML...");
+				System.exit(-1);
+			}
+			// else, not running in debug mode, so don't exit after exception  
+			System.out.println("***Turn debugging on (run GLI with -debug) to view the XML contents that could not be parsed.");
+			DebugStream.printStackTrace(exception);
+		}
+		catch (Exception exception)
+		{
+			DebugStream.printStackTrace(exception);
+		}
+
+		return document;
+	}
+
+	/**
+	 * Displays the line (string) where the SAXParseException occurred, given a
+	 * String of the entire xml that was being parsed and the SAXParseException
+	 * object that was caught. The messages are printed to DebugStream, so run
+	 * GLI/FLI with -debug to view this output.
+	 * 
+	 * @param xmlContents
+	 *            is the entire xml that was being parsed when the exception
+	 *            occurred
+	 * @param e
+	 *            is the SAXParseException object that was thrown upon parsing
+	 *            the xmlContents.
+	 */
+	public static void showXMLParseFailureLine(SAXParseException e, String xmlContents)
+	{
+
+		// There should be no characters at all that preceed the <?xml>... bit. 
+		// The first check is for starting spaces:
+		if (xmlContents.startsWith("\n") || xmlContents.startsWith(" ") || xmlContents.startsWith("\t"))
+		{
+			DebugStream.println("ERROR: illegal start of XML. Space/tab/newline should not preceed xml declaration.\n");
+			DebugStream.println("xmlContents (length is " + xmlContents.length() + "):\n" + xmlContents);
+			return; // nothing more to do, first error identified
+		}
+
+		// the actual line (String literal) where parsing failed and the SAXParseException occurred.
+		String line = "";
+		int linenumber = e.getLineNumber();
+		DebugStream.print("\n****SAXParseException on LINE NUMBER: " + linenumber);
+		if (DebugStream.isDebuggingEnabled())
+		{
+			if (linenumber != -1)
+			{
+				String[] lines = xmlContents.split("\n");
+				if (lines.length > 0)
+				{
+					DebugStream.println(" (number of lines: " + lines.length + ")");
+					if (lines.length >= linenumber)
+					{
+						line = lines[linenumber - 1];
+					}
+					else
+					{ // error is past the last line
+						line = "Error is past the last line (" + lines.length + "): " + lines[lines.length - 1];
+					}
+				}
+				else
+				{
+					DebugStream.print("\n");
+				}
+				lines = null;
+
+				DebugStream.println("The parsing error occurred on this line:\n***********START\n" + line + "\n***********END");
+				DebugStream.println("SAXParseException message: " + e.getMessage() + "\n");
+
+				// Uncomment if you want to print out the entire contents of the XML doc:
+				//DebugStream.println("\n\nThis was the XML:\n*********START\n" 
+				//  + xmlContents + "\n************END\n");
+			}
+			else
+			{ // no particular line number, print out all the xml so debugger can inspect it
+				DebugStream.println("Encountered a SAX exception when parsing the following:\n*********START\n" + xmlContents + "\n************END\n");
+			}
+			// Exit to let the user view the erroneous line/xml before it goes past the screen buffer?
+			DebugStream.println("\nDebug mode: Exiting the program as there was trouble parsing the XML...");
+			System.exit(-1);
+		}
+		else
+		{ // not running in debug mode
+			System.out.println("***Turn debugging on (run GLI with -debug) to view the XML contents/line that could not be parsed.");
+		}
+	}
+
+	static public StringBuffer readXMLStream(InputStream input_stream)
+	{
+		StringBuffer xml = new StringBuffer("");
+
+		try
+		{
+			InputStreamReader isr = new InputStreamReader(input_stream, "UTF-8");
+			BufferedReader buffered_in = new BufferedReader(isr);
+
+			String line = "";
+			boolean xml_content = false;
+			while ((line = buffered_in.readLine()) != null)
+			{
+				if (xml_content)
+				{
+					xml.append(line);
+					xml.append("\n");
+				}
+				else if (line.trim().startsWith("<?xml"))
+				{
+					xml_content = true;
+					xml.append(line);
+					xml.append("\n");
+				}
+				else
+				{
+					System.err.println(line);
+				}
+			}
+			buffered_in = null;
+		}
+		catch (Exception error)
+		{
+			System.err.println("Failed when trying to parse XML stream");
+			error.printStackTrace();
+		}
+
+		return xml;
+	}
+
+	/**
+	 * Removes characters that are invalid in XML (see
+	 * http://www.w3.org/TR/2000/REC-xml-20001006#charsets)
+	 */
+	static public String removeInvalidCharacters(String text)
+	{
+		char[] safe_characters = new char[text.length()];
+		int j = 0;
+
+		char[] raw_characters = new char[text.length()];
+		text.getChars(0, text.length(), raw_characters, 0);
+		for (int i = 0; i < raw_characters.length; i++)
+		{
+			char character = raw_characters[i];
+			if ((character >= 0x20 && character <= 0xD7FF) || character == 0x09 || character == 0x0A || character == 0x0D || (character >= 0xE000 && character <= 0xFFFD) || (character >= 0x10000 && character <= 0x10FFFF))
+			{
+				safe_characters[j] = character;
+				j++;
+			}
+		}
+
+		return new String(safe_characters, 0, j);
+	}
+
+	static public void setElementTextValue(Element element, String text)
+	{
+		// Remove all text node children
+		NodeList children_nodelist = element.getChildNodes();
+		for (int i = children_nodelist.getLength() - 1; i >= 0; i--)
+		{
+			Node child_node = children_nodelist.item(i);
+			if (child_node.getNodeType() == Node.TEXT_NODE)
+			{
+				element.removeChild(child_node);
+			}
+		}
+
+		// Add a new text node
+		if (text != null)
+		{
+			element.appendChild(element.getOwnerDocument().createTextNode(text));
+		}
+	}
+
+	/**
+	 * Set the #text node value of some element.
+	 * 
+	 * @param element
+	 *            the Element whose value we wish to set
+	 * @param value
+	 *            the new value for the element as a String Soon to be
+	 *            deprecated!
+	 */
+	static final public void setValue(Element element, String value)
+	{
+		// Remove any existing child node(s)
+		clear(element);
+		// Add new text node.
+		if (value != null)
+		{
+			element.appendChild(element.getOwnerDocument().createTextNode(value));
+		}
+	}
+
+	static public void indentXML(Element elem, int depth)
+	{
+		Document doc = elem.getOwnerDocument();
+
+		String startIndentString = "\n";
+		for (int i = 0; i < depth; i++)
+		{
+			startIndentString += "\t";
+		}
+		Node startTextNode = doc.createTextNode(startIndentString);
+
+		String endIndentString = "\n";
+		for (int i = 0; i < depth - 1; i++)
+		{
+			endIndentString += "\t";
+		}
+		Node endTextNode = doc.createTextNode(endIndentString);
+
+		boolean found = false;
+		Node child = elem.getFirstChild();
+		while (child != null)
+		{
+			// first clear all empty text nodes (those containing space characters like \n,\r,\t and such)
+			if(child.getNodeType() == Node.TEXT_NODE && child.getNodeValue().matches("^\\s*$")) 
+			{
+				Node spaceTextNode = child;
+				child = child.getNextSibling();
+				elem.removeChild(spaceTextNode);
+				
+				if(child == null) break;
+			}
+
+			// now process normal element nodes as intended
+			if (child.getNodeType() == Node.ELEMENT_NODE)
+			{
+				found = true;
+				break;
+			}
+			child = child.getNextSibling();
+		}
+
+		if (found)
+		{
+			elem.appendChild(endTextNode);
+		}
+
+		child = elem.getFirstChild();
+		while (child != null)
+		{
+			// Again, need to first clear all empty text nodes (those containing space characters like \n,\r,\t and such)
+			// because the first while loop above would break out when it found an element node and wouldn't have got rid
+			// of all the empty text nodes yet.
+			// This time, beware not to delete the special end and start empty textnodes just added, since 
+			// they've been created and inserted specifically.
+			if(child != endTextNode && child != startTextNode 
+			   && child.getNodeType() == Node.TEXT_NODE && child.getNodeValue().matches("^\\s*$"))
+			{
+				Node spaceTextNode = child;
+				child = child.getNextSibling();
+				elem.removeChild(spaceTextNode);
+
+				if(child == null) break;
+			}
+
+			// go back to processing normal element nodes as intended
+			if (child.getNodeType() == Node.ELEMENT_NODE)
+			{
+				elem.insertBefore(startTextNode.cloneNode(false), child);
+				indentXML((Element) child, depth + 1);
+			}
+			child = child.getNextSibling();
+		}
+	}
+
+	/**
+	 * Write an XML document to a given file with the text node of the specified
+	 * element unescaped
+	 */
+	static public void writeXMLFile(File xml_file, Document document, String[] nonEscapingTagNames)
+	{
+		indentXML(document.getDocumentElement(), 1);
+		try
+		{
+			OutputStream os = new FileOutputStream(xml_file);
+			// Create an output format for our document.
+			OutputFormat f = new OutputFormat(document);
+			f.setEncoding("UTF-8");
+			f.setIndenting(true);
+			f.setLineWidth(0); // Why isn't this working!
+			f.setPreserveSpace(true);
+			if (nonEscapingTagNames != null)
+			{
+				f.setNonEscapingElements(nonEscapingTagNames);
+			}
+			// Create the necessary writer stream for serialization.
+			OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
+			Writer w = new BufferedWriter(osw);
+			// Generate a new serializer from the above.
+			XMLSerializer s = new XMLSerializer(w, f);
+			s.asDOMSerializer();
+			// Finally serialize the document to file.
+			s.serialize(document);
+			// And close.
+			os.close();
+		}
+		catch (Exception exception)
+		{
+			DebugStream.printStackTrace(exception);
+		}
+	}
+
+	/** Write an XML document to a given file */
+	static public void writeXMLFile(File xml_file, Document document)
+	{
+		writeXMLFile(xml_file, document, null);
+	}
+
+	public static void printXMLNode(Node e)
+	{
+		printXMLNode(e, 0);
+	}
+
+	public static void printXMLNode(Node e, int depth)
+	{ //recursive method call using DOM API...
+
+		for (int i = 0; i < depth; i++)
+			System.out.print(' ');
+
+		if (e.getNodeType() == Node.TEXT_NODE)
+		{
+			//System.out.println("text") ;
+			if (e.getNodeValue() != "")
+			{
+				System.out.println(e.getNodeValue());
+			}
+			return;
+		}
+
+		System.out.print('<');
+		System.out.print(e.getNodeName());
+		NamedNodeMap attrs = e.getAttributes();
+		if (attrs != null)
+		{
+			for (int i = 0; i < attrs.getLength(); i++)
+			{
+				Node attr = attrs.item(i);
+				System.out.print(' ');
+				System.out.print(attr.getNodeName());
+				System.out.print("=\"");
+				System.out.print(attr.getNodeValue());
+				System.out.print('"');
+			}
+		}
+		NodeList children = e.getChildNodes();
+
+		if (children == null || children.getLength() == 0)
+			System.out.println("/>");
+		else
+		{
+
+			System.out.println('>');
+
+			int len = children.getLength();
+			for (int i = 0; i < len; i++)
+			{
+				printXMLNode(children.item(i), depth + 1);
+			}
+
+			for (int i = 0; i < depth; i++)
+				System.out.print(' ');
+
+			System.out.println("</" + e.getNodeName() + ">");
+		}
+
+	}
+
+	public static String xmlNodeToString(Node e)
+	{
+		StringBuffer sb = new StringBuffer("");
+		xmlNodeToString(sb, e, true, "\t", 2);
+		return sb.toString();
+	}
+
+	public static void xmlNodeToString(StringBuffer sb, Node e, boolean indent, String indentString, int depth)
+	{
+
+	    if (e.getNodeType() == Node.CDATA_SECTION_NODE)
+		{
+			if (e.getNodeValue() != "")
+			{
+				String text = e.getNodeValue();				
+				sb.append("<![CDATA[");
+				sb.append(text);
+				sb.append("]]>");
+			}
+			return;
+		}
+
+	    if (e.getNodeType() == Node.TEXT_NODE)
+		{
+			if (e.getNodeValue() != "")
+			{
+				String text = e.getNodeValue();
+				text = text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("[\\n\\r\\t\\s]*$", "");
+				for (Character c : text.toCharArray())
+				{
+					if (c.equals('\n'))
+					{
+						text = text.replaceAll("^[\\n\\r\\t\\s]*", "");
+						break;
+					}
+
+					if (!Character.isWhitespace(c))
+					{
+						break;
+					}
+				}
+				sb.append(text);
+			}
+			return;
+		}
+		
+		if (e.getNodeType() == Node.COMMENT_NODE)
+		{
+			if (e.getNodeValue() != "")
+			{
+				sb.append("<!--\n" + e.getNodeValue().trim() + "\n-->\n");
+			}
+			return;
+		}
+
+		if (indent)
+		{
+			for (int i = 0; i < depth; i++)
+			{
+				sb.append(indentString);
+			}
+		}
+
+		sb.append('<');
+		sb.append(e.getNodeName());
+		NamedNodeMap attrs = e.getAttributes();
+		if (attrs != null)
+		{
+			for (int i = 0; i < attrs.getLength(); i++)
+			{
+				Node attr = attrs.item(i);
+				sb.append(' ');
+				sb.append(attr.getNodeName());
+				sb.append("=\"");
+				sb.append(attr.getNodeValue().replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"));
+				sb.append('"');
+			}
+		}
+		NodeList children = e.getChildNodes();
+
+		boolean hasElements = false;
+		boolean indentSwapped = false;
+		for (int i = 0; i < children.getLength(); i++)
+		{
+			if (children.item(i).getNodeType() == Node.ELEMENT_NODE)
+			{
+				hasElements = true;
+			}
+			if ((children.item(i).getNodeType() == Node.TEXT_NODE || children.item(i).getNodeType() == Node.CDATA_SECTION_NODE) && indent)
+			{
+				if (children.item(i).getNodeValue().trim().length() > 0)
+				{
+					indentSwapped = true;
+					indent = false;
+				}
+			}
+		}
+
+		if (children == null || children.getLength() == 0)
+		{
+			sb.append("/>");
+
+			if (indent)
+			{
+				sb.append("\n");
+			}
+		}
+		else
+		{
+			sb.append(">");
+			if (hasElements && indent)
+			{
+				sb.append("\n");
+			}
+
+			int len = children.getLength();
+			for (int i = 0; i < len; i++)
+			{
+				xmlNodeToString(sb, children.item(i), indent, indentString, depth + 1);
+			}
+
+			if (indent)
+			{
+				for (int i = 0; i < depth; i++)
+				{
+					sb.append(indentString);
+				}
+			}
+
+			sb.append("</" + e.getNodeName() + ">");
+
+			if ((hasElements && indent) || indentSwapped)
+			{
+				sb.append("\n");
+			}
+		}
+	}
+
+	public static String xmlNodeToStringWithoutIndenting(Node e)
+	{
+		StringBuffer sb = new StringBuffer("");
+		xmlNodeToStringWithoutNewline(sb, e, -1);
+		return sb.toString();
+	}
+
+	public static String xmlNodeToStringWithoutNewline(Node e)
+	{
+		StringBuffer sb = new StringBuffer("");
+		xmlNodeToStringWithoutNewline(sb, e, 0);
+		return sb.toString();
+	}
+
+	private static void xmlNodeToStringWithoutNewline(StringBuffer sb, Node e, int depth)
+	{
+
+		for (int i = 0; i < depth; i++)
+		{
+			sb.append(' ');
+		}
+
+		if (e.getNodeType() == Node.TEXT_NODE)
+		{
+			if (e.getNodeValue() != "")
+			{
+				sb.append(e.getNodeValue().replaceAll("&", "&amp;").replaceAll("<", "&lt;").replace(">", "&gt;"));
+			}
+			return;
+		}
+
+		if (e.getNodeType() == Node.COMMENT_NODE)
+		{
+			if (e.getNodeValue() != "")
+			{
+				sb.append("<!--" + e.getNodeValue() + "-->");
+			}
+			return;
+		}
+
+		sb.append('<');
+		sb.append(e.getNodeName());
+		NamedNodeMap attrs = e.getAttributes();
+		if (attrs != null)
+		{
+			for (int i = 0; i < attrs.getLength(); i++)
+			{
+				Node attr = attrs.item(i);
+				sb.append(' ');
+				sb.append(attr.getNodeName());
+				sb.append("=\"");
+				sb.append(attr.getNodeValue());
+				sb.append('"');
+			}
+		}
+		NodeList children = e.getChildNodes();
+
+		if (children == null || children.getLength() == 0)
+			sb.append("/>");
+		else
+		{
+
+			sb.append(">");
+
+			int len = children.getLength();
+			for (int i = 0; i < len; i++)
+			{
+				if (depth >= 0)
+				{
+					xmlNodeToStringWithoutNewline(sb, children.item(i), depth + 1);
+				}
+				else
+				{
+					xmlNodeToStringWithoutNewline(sb, children.item(i), depth);
+				}
+			}
+
+			for (int i = 0; i < depth; i++)
+				sb.append(' ');
+
+			sb.append("</" + e.getNodeName() + ">");
+		}
+	}
+
+
+    
+    // This method will convert an Element to a String too, like xmlNodeToString() above.
+    // But for a document root element (doc.getDocumentElement()), this method will additionally
+    // return its processing instruction line at the start (<?xml ... ?>).
+    // This method copied into GLI from src/java/org/greenstone/gsdl3/util/GSXML.java
+    public static String elementToString(Element e, boolean indent)
+    {
+	String str = "";
+	try
+	    {
+		TransformerFactory tf = TransformerFactory.newInstance();
+		Transformer trans = tf.newTransformer();
+		StringWriter sw = new StringWriter();
+		if (indent)
+		    {
+			trans.setOutputProperty(OutputKeys.INDENT, "yes");
+		    }
+		else
+		    {
+			trans.setOutputProperty(OutputKeys.INDENT, "no");
+		    }
+		trans.transform(new DOMSource(e), new StreamResult(sw));
+		str = sw.toString();
+	    }
+	catch (Exception ex)
+	    {
+		str += "Exception: couldn't write " + e + " to log";
+	    }
+	finally
+	    {
+		return str;
+	    }
+    }
+}
