/**
 *#########################################################################
 * 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; 

  // Gs2 and 3 use the same config file format - same template - but will save in user folder as config2/3 so you can
  // save settings for version 2 and 3.
  static final public String CONFIG_XML_TEMPLATE = "config.xml";
  static final public String CONFIG_XML_2 = "config2.xml";
  static final public String CONFIG_XML_3 = "config3.xml";
  static final public String CONFIG_REMOTE_XML = "configRemote.xml";
    static final public String CONFIG_WEBSWING_XML = "configWebswing.xml";
    static final public String TEMPLATE_CONFIG_PREFIX = "xml/";
     static final public String FEDORA_CONFIG_PREFIX = "fedora-";
 
    /** The string identifying an argument's name attribute. */
    static final private String ARGUMENT_NAME = "name";

    /** 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 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;
    /** The name of the general Gatherer configuration file. 
     Gatherer will set this to configRemote/2/3 as necessary */
  static public String config_xml = CONFIG_XML_2; 
    /** The name of the general Gatherer configuration template.
    Gatherer will change this to xml/configRemote.xml if isGSDLRemote */
  static public String template_config_xml = TEMPLATE_CONFIG_PREFIX + CONFIG_XML_TEMPLATE; 

    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 web folder which contains sites and interfaces */
    static public String gsdl3_web_path = "";
    /** If we are using GLI in Greenstone 3, the path to gsdl3 web folder which contains WEB-INF, cgi, ext etc */
    static public String gsdl3_writableweb_path = "";
    /** If we are using GLI in Greenstone 3, the path to gsdl3 src directory (== greenstone3 dir)*/
    static public String gsdl3_src_path = "";

    static public FedoraInfo fedora_info = null;

  // if gli has been started with lang arg
  static public String startup_lang = 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;
  static public URL local_url = null;
    
    /** Constructor.
     * @param gsdl_path The path to the Greenstone directory as a <strong>String</strong>.
     * @param gsdl3_web_path The path to the Greenstone 3 home (web) directory as a <strong>String</strong>.
     * @param gsdl3_writableweb_path The path to the writable 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_web_path, String gsdl3_writableweb_path, String gsdl3_src_path, 
                       String site_name, String servlet_path,  FedoraInfo fedora_info, String startup_lang) 
    {
	super();
	self = this; // allows Configuration to statically refer to itself in the static methods

	try {
	    // this mainly needed for webswing, where the paths are unix style, but if running on windows we need to convert them
          this.gli_user_directory_path = new File(gli_user_directory_path).getCanonicalPath()+File.separator;
          if(gsdl_path != null) {
	    this.gsdl_path = new File(gsdl_path).getCanonicalPath()+File.separator;
          }

          if (gsdl3_web_path != null) {
            this.gsdl3_web_path = new File(gsdl3_web_path).getCanonicalPath()+File.separator;
          }
          if (gsdl3_writableweb_path != null) {
            this.gsdl3_writableweb_path = new File(gsdl3_writableweb_path).getCanonicalPath()+File.separator;
          }
          if(gsdl3_src_path != null) { // null for GEMS standalone launch
            this.gsdl3_src_path = new File(gsdl3_src_path).getCanonicalPath()+File.separator;
          }
	} catch(IOException e) {
          System.err.println("IOException with supplied path arguments:"+e.getMessage());
          System.exit(0);
	}
	this.site_name = site_name;
	this.servlet_path = servlet_path;

	this.fedora_info = fedora_info;
        this.startup_lang = startup_lang;

	// Try to load the configuration file from the user specific location
	String config_xml_file_name = this.config_xml;

        // I think this has already been set by Gatherer
	// 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(this.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);

        // set the lang if necessary
        if (startup_lang != null) {
          setLocale("general.locale", true, new Locale(startup_lang));
        }
	// 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);
		}
	    }
	}

        // Read local url from config file, if using webswing
        if (Gatherer.isWebswing) {
          String local_url_string = getString("general.local_url", true);
          if (!local_url_string.equals("")) {
		try {
		    local_url = new URL(local_url_string);
		}
		catch (MalformedURLException exception) {
		    DebugStream.printStackTrace(exception);
		}
	    }
	}
         
        if (Gatherer.GS3) {
	    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_web_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);	    
	} 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;
	}

        // version might be like 3.11 or svn-trunk
	String new_version = new_document.getDocumentElement().getAttribute(StaticStrings.VERSION_ATTRIBUTE);
	String old_version = old_document.getDocumentElement().getAttribute(StaticStrings.VERSION_ATTRIBUTE);
        String new_path = (Gatherer.GS3 ? gsdl3_src_path : gsdl_path);
        String old_path = old_document.getDocumentElement().getAttribute("gs_path");

	if (new_version.equals(old_version) && new_path.equals(old_path)) {
          // save version, same install path - don't need to update
          return false;
        }

        // if we got here, we are a different installation of gs. convert the file
	System.err.println("Converting user config.xml from version " + old_version + " ("+old_path+") to version " + new_version + " ("+new_path+")...");

	// 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"));
        // set the correct path in new_document
        new_document.getDocumentElement().setAttribute("gs_path", new_path);
        
	// 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()) && !new_argument_element_name.equals("general.local_url")) {
		    
		    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;
	if (Gatherer.isWebswing) {
	    title += " (webswing)";
	}
	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(mapping_element.getAttribute(StaticStrings.FILE_ATTRIBUTE));//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;

        // this should have been set already
	// 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)));

	//
	// Commenting out the following, as did not give the desired result in WebSwing GLI
	// => Was causing the disabled text to tabs (e.g., when a collection is closed) to be bright green text
	//    If comment this out has, in turn, an undesirable effect on Desktop GLI, then wrap up in an if-statement
	//
	//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)));
    }
}
