Index: /trunk/gli/src/org/greenstone/gatherer/gems/Attribute.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/Attribute.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/Attribute.java	(revision 8270)
@@ -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.gems;
+
+public class Attribute 
+    implements Comparable {
+    public String language = null;
+    public String name = null;
+    public String value = null;
+    public String text = null;
+    private boolean language_specific = false;
+    public Attribute(String name, String value) {
+	this.name = name;
+	this.value = value;
+    }
+    public Attribute(String name, String language, String value) {
+	this.language = language;
+	this.language_specific = true;
+	this.name = name;
+	this.value = value;
+    }
+    public int compareTo(Object other) {
+	return toString().compareTo(other.toString());
+    }
+    public boolean equals(Object other) {
+	return toString().equals(other.toString());
+    }
+    public String get(int column) {
+	switch(column) {
+	case 0:
+	    return name;
+	case 1:
+	    if(language_specific) {
+		return language;
+	    }
+	default:
+	    return value;
+	}
+    }
+    public String toString() {
+	if(text == null) {
+	    if(language_specific) {
+		text = name + " [" + language + "] = " + value;
+	    }
+	    else {
+		text = name + " = " + value;
+	    }
+	}
+	return text;
+    }
+}
Index: /trunk/gli/src/org/greenstone/gatherer/gems/AttributeTableModel.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/AttributeTableModel.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/AttributeTableModel.java	(revision 8270)
@@ -0,0 +1,211 @@
+/**
+ *#########################################################################
+ *
+ * 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.gems;
+
+import java.awt.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.table.*;
+import org.greenstone.gatherer.util.Utility;
+
+
+/**
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class AttributeTableModel 
+    extends AbstractTableModel {
+    private Attribute last_attribute = null;
+    private int last_index = -1;
+    private int longest[] = null;
+    private int column_zero_width = 0;
+    private JScrollPane scroll_pane = null;
+    private JTable table = null;
+    private String default_value = null;
+    private String names[] = null;
+    private TreeSet attributes = null;
+    static final private int table_width = 550;
+    public AttributeTableModel(TreeSet attributes, String one, String two, String default_value) {
+	super();
+	this.attributes = attributes;
+	this.default_value = default_value;
+	this.longest = new int[2];
+	this.names = new String[2];
+	names[0] = one;
+	names[1] = two;
+    }
+
+    public AttributeTableModel(TreeSet attributes, String one, String two, String three, String default_value) {
+	super();
+	this.attributes = attributes;
+	this.default_value = default_value;
+	this.longest = new int[3];
+	this.names = new String[3];
+	names[0] = one;
+	names[1] = two;
+	names[2] = three;
+    }
+    public int add(Attribute attribute) {
+	attributes.add(attribute);
+	// Determine the row index
+	boolean found = false;
+	int index = 0;
+	Iterator iterator = attributes.iterator();
+	for(int i = 0; !found && iterator.hasNext(); i++) {
+	    if(attribute == iterator.next()) {
+		found = true;
+		index = i;
+	    }
+	}
+	// Fire event
+	fireTableRowsInserted(index, index);
+	return index;
+    }
+
+    public boolean contains(String value, int column) {
+	boolean result = false;
+	Iterator iterator = attributes.iterator();
+	while(iterator.hasNext() && !result) {
+	    Attribute attribute = (Attribute) iterator.next();
+	    if(value.equals(attribute.get(column))) {
+		result = true;
+	    }
+	}  
+	return result;
+    }
+
+    public Attribute getAttribute(int row) {
+	if(row != last_index) {
+	    last_index = row;
+	    last_attribute = null;
+				// Retrieve the value from the set using its iterator
+	    Iterator iterator = attributes.iterator();
+	    for(int i = 0; i < row && iterator.hasNext(); i++) {
+		iterator.next();
+	    }
+	    if(iterator.hasNext()) {
+		last_attribute = (Attribute) iterator.next();
+	    }
+	    iterator = null;
+	}
+	return last_attribute;
+    }
+
+    public int getColumnCount() {
+	return names.length;
+    }
+
+    public String getColumnName(int col) {
+	return names[col];
+    }
+
+    public int getRowCount() {
+	int count = 0;
+	if(attributes != null) {
+	    count = attributes.size();
+	}
+	return count;
+    }
+
+    public Object getValueAt(int row, int col) {
+	String value = null;
+	Attribute attribute = getAttribute(row);
+	if(attribute != null) {
+	    value = attribute.get(col);
+	}
+	if(value == null) {
+	    value = default_value;
+	}
+	// Update table column width if necessary
+	if(value.length() > longest[col]) {
+	    Component component = table.getCellRenderer(row, col).getTableCellRendererComponent(table, value, false, false, 0, 0);
+	    int width = component.getPreferredSize().width + 10;
+	    TableColumn column = table.getColumnModel().getColumn(col);
+	    if(width > column.getPreferredWidth()) {
+		// Calculate default column width
+		int default_width = 0;
+		int total_width = scroll_pane.getSize().width;
+		if(names.length == 2) {
+		    if(col == 0) {
+			default_width = total_width / 3;
+		    }
+		    else {
+			default_width = 2 * (total_width / 3);
+		    }
+		}
+		else {
+		    switch(col) {
+		    case 0:
+			default_width = (2 * total_width) / 9;
+			break;
+		    case 1:
+			default_width = total_width / 9;
+			break;
+		    default:
+			default_width = 2 * (total_width / 3);
+			break;
+		    }
+		}
+		if(width > default_width) {
+		    column.setPreferredWidth(width);
+		}
+		else {
+		    column.setPreferredWidth(default_width);
+		}
+	    }
+	    longest[col] = value.length();
+	}
+	return Utility.stripNL(value);
+    }
+    public void removeRow(final int row) {
+	///ystem.err.println("Remove row " + row);
+	Attribute attribute = getAttribute(row);
+	if(attribute != null) {
+	    attributes.remove(attribute);
+				// Arg... this caused me a headache for hours.
+	    last_index = -1;
+	    fireTableRowsDeleted(row, row);
+	}
+    }
+    public void setScrollPane(JScrollPane scroll_pane) {
+	this.scroll_pane = scroll_pane;
+    }
+    public void setTable(JTable table) {
+	this.table = table;
+	this.longest = new int[getColumnCount()];
+    }
+}
Index: /trunk/gli/src/org/greenstone/gatherer/gems/ElementWrapper.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/ElementWrapper.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/ElementWrapper.java	(revision 8270)
@@ -0,0 +1,213 @@
+/**
+ *#########################################################################
+ *
+ * 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.gems;
+
+/**************************************************************************************
+ * Title:        Gatherer
+ * Description:  The Gatherer: a tool for gathering and enriching a digital collection.
+ * Company:      The University of Waikato
+ * Written:        /  /01
+ * Revised:      03/10/02 - Commented
+ **************************************************************************************/
+import java.util.TreeSet;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Troolean;
+import org.greenstone.gatherer.util.Utility;
+import org.w3c.dom.*;
+
+/** This class provides a convience wrapper around a DOM model Element to allow Components such as the MetadataTable to display this information properly.
+ * @author John Thompson
+ * @version 2.3
+ */
+public class ElementWrapper
+    implements Comparable {
+    /** The DOM element this wrapper is wrapped around. */
+    private Element element = null;
+    /** A string prefix identifying the metadata set namespace. */
+    private String namespace = "";
+
+    private Troolean is_extracted = new Troolean();
+    /** Constructor for elements with no namespace necessary.
+     * @param element The DOM <strong>Element</strong> this is to be based on.
+     */
+    public ElementWrapper(Element element) {
+	this.element = element;
+	Element parent =  (Element)element.getParentNode();
+	if (parent != null) {
+	    this.namespace = parent.getAttribute("namespace");
+	}
+
+    }
+
+    public void addAttribute(String name, String value) {
+	MSMUtils.addElementAttribute(element, name, "en", value);
+    }
+
+    public void addAttribute(String name, String language, String value) {
+	MSMUtils.addElementAttribute(element, name, language, value);
+    }
+
+    /** Create a copy of this element wrapper.
+     * @return A new <strong>ElementWrapper</strong> based on the same element as this one.
+     */
+    public ElementWrapper copy() {
+	Element element_copy = (Element) element.cloneNode(true);
+	return new ElementWrapper(element_copy);
+    }
+    /** Compare two element wrappers for ordering.
+     * @param object An <strong>Object</strong> which is most likely another element wrapper to be compared with.
+     * @return An <i>int</i> indicating order, -1, 0, 1 if this element wrapper is less than, equal to or greater than the given object.
+     */
+    public int compareTo(Object object) {
+	return toString().compareTo(object.toString());
+    }
+    /** Decrement the number of occurances of this metadata element.
+     * @see org.greenstone.gatherer.msm.MSMUtils
+     */
+    public void dec() {
+	MSMUtils.setOccurance(element, -1);
+    }
+    /** Test if two ElementWrappers are equal.
+     * @param object The <strong>Object</strong> to test against.
+     */
+   public boolean equals(Object object) {
+      if(object == null) {
+	 return false;
+      }
+      if(object instanceof ElementWrapper) {
+	 String our_full_name = MSMUtils.getFullName(element, namespace);
+	 String their_full_name = MSMUtils.getFullName(((ElementWrapper)object).getElement());
+	 return our_full_name.equals(their_full_name);
+      }
+      return toString().equals(object.toString());
+    }
+    /** Retrieve the attributes associated with the element this element wrapper is built around.
+     * @return A <strong>TreeSet</strong> of the attributes.
+     * @see org.greenstone.gatherer.msm.MSMUtils
+     */
+    public TreeSet getAttributes() {
+	return MSMUtils.getAttributes(element);
+    }
+    /** Retrieve the element this is wrapped around.
+     * @return A DOM <strong>Element</strong> which represents a metadata element.
+     */
+    public Element getElement() {
+	return element;
+    }
+    /** Retrieve the identity of this element (not necessary the same as this elements name). Identity is language and locale dependant.
+     * @return The identity as a <strong>String</strong>.
+     * @see org.greenstone.gatherer.msm.MSMUtils
+     */
+    public String getIdentity() {
+	return MSMUtils.getIdentifier(element);
+    }
+    /** Retrieve the name of this element. Name is unique.
+     * @return The name as a <strong>String</strong>.
+     * @see org.greenstone.gatherer.msm.MSMUtils
+     */
+    public String getName() {
+	return MSMUtils.getFullName(element, namespace);
+    }
+    /** Retrieve the namespace prefix for this element wrapper.
+     * @return A <strong>String</strong> containing the namespace or "" if there is no namespace for this element.
+     * @see org.greenstone.gatherer.msm.MSMUtils
+     */
+    public String getNamespace() {
+	if (!namespace.equals("")) {
+	    return namespace;
+	}
+	String name = getName();
+	int pos;
+	if((pos = name.indexOf(MSMUtils.NS_SEP)) != -1) {
+	    return name.substring(0, pos);
+	}
+	else {
+	    return "";
+	}
+    }
+    /** Look for the occurances 'field' of the element and return it if found.
+     * @return An <i>int</i> which matches the number in the occurances attribute of the element, or 0 if no such attribute.
+     * @see org.greenstone.gatherer.msm.MSMUtils
+     */
+    public int getOccurances() {
+	return MSMUtils.getOccurances(element);
+    }
+    /** This method is essentially the same as getDescription() in that it does indeed return this metaelements description. However this method uses the Utility function formatHTMLWidth() to ensure the String can be displayed in a tool-tip window using html markup.
+     * @return A String containing the HTML formatted versions of definition and content (comment).
+     * @see org.greenstone.gatherer.msm.MSMUtils
+     * @see org.greenstone.gatherer.util.Utility
+     */
+    public String getToolTip() {
+	// Add HTML formatting
+	return Utility.formatHTMLWidth(MSMUtils.getDescription(element), 60);
+    }
+
+    public boolean hasValueTree() {
+	return element.getAttribute(StaticStrings.VALUE_TREE_ATTRIBUTE).equalsIgnoreCase(StaticStrings.TRUE_STR);
+    }
+
+    /** Increment the number of occurances of this metadata element.
+     * @see org.greenstone.gatherer.msm.MSMUtils
+     */
+    public void inc() {
+	MSMUtils.setOccurance(element, 1);
+    }
+
+    public boolean isExtracted() {
+	if(!is_extracted.isDecided()) {
+	    String raw_namespace = namespace;
+	    if(raw_namespace != null && raw_namespace.length() > 0 && !raw_namespace.endsWith(StaticStrings.STOP_CHARACTER)) {
+		raw_namespace = raw_namespace + StaticStrings.STOP_CHARACTER;
+	    }
+	    is_extracted.set(raw_namespace == null || raw_namespace.equals(StaticStrings.EMPTY_STR) || raw_namespace.equals(StaticStrings.EXTRACTED_NAMESPACE));
+	}
+	return is_extracted.isTrue();
+    }
+
+    public boolean isHierarchy() {
+	return element.getAttribute(StaticStrings.HIERARCHY_ATTRIBUTE).equalsIgnoreCase(StaticStrings.TRUE_STR);
+    }
+
+    /** Removes an Attribute node from the element. */
+    public boolean removeAttribute(String name, String language, String value) {
+	return MSMUtils.removeElementAttribute(element, name, language, value);
+    }
+
+    public String toString() {
+	return getNamespace() + MSMUtils.NS_SEP + getIdentity();
+    }
+}
Index: /trunk/gli/src/org/greenstone/gatherer/gems/GEMS.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/GEMS.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/GEMS.java	(revision 8270)
@@ -0,0 +1,2288 @@
+/**
+ *#########################################################################
+ *
+ * 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.gems;
+
+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.text.*;
+import javax.swing.tree.*;
+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.ComboArea;
+import org.greenstone.gatherer.gui.GComboBox;
+import org.greenstone.gatherer.gui.GLIButton;
+import org.greenstone.gatherer.gui.SmarterTable;
+import org.greenstone.gatherer.gui.SmarterTree;
+import org.greenstone.gatherer.gui.ModalDialog;
+import org.greenstone.gatherer.gui.NonWhitespaceField;
+import org.greenstone.gatherer.gui.TransformCharacterTextField;
+import org.greenstone.gatherer.help.HelpFrame;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.Utility;
+import org.w3c.dom.*;
+
+/** Provides a GUI and relevant suite of tools for the editing of the metadata set associated with this collection. Again I have tried to capture a file manager type feel, with a tree showing the various set-element relations to the left of the dialog, and the right side showing details on the current tree selection. When a set is selected these details include a list of attributes, while when an element is selected this list is joined by another showing assigned values. In order for the editor to be stable and consistant with the rest of the tool, care must be taken for fire appropriate events whenever the sets are changed. There is also the addded complexity of determining what actions have to occur in order for a users edit of an assigned value to be completed, i.e. if the user chooses to remove a value then a call must be made to record_set.root.removeMetadata() to ensure all such values are actually removed, so it is not enough just to remove the value from the value model. 
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3b
+ */ 
+public class GEMS
+    extends JFrame
+{
+    static final public int ADD_SET    = 0;
+    static final public int NORMAL     = 1;
+    static final public int REMOVE_SET = 2;
+    /** The default size of the editor dialog. */
+    static final private Dimension ADD_ELEMENT_SIZE = new Dimension(400,125);
+    static final private Dimension ADD_FILE_SIZE = new Dimension(400,125);
+    static final private Dimension ADD_SET_SIZE = new Dimension(400,125);
+    static final private Dimension ADD_OR_EDIT_ATTRIBUTE_SIZE = new Dimension(600,325);
+    static final private Dimension ADD_OR_EDIT_VALUE_SIZE = new Dimension(600,440);
+    static final private Dimension COMPONENT_SIZE = new Dimension(300,30);
+    static final private Dimension SIZE = new Dimension(800,480);
+    static final private String BLANK = "blank";
+    static final private String ELEMENT = "element";
+    static final private String PROFILE = "profile";
+    static final private String SET = "set";
+    static final private String VALUES = "values";
+
+    static private Configuration config = null;
+    static public MetadataSetManager msm = new MetadataSetManager();
+
+    private AddElementActionListener add_element_action_listener = null;
+    private AddFileActionListener add_file_action_listener = null;
+    private AddSetActionListener add_set_action_listener = null;
+    /** The class used to handle add or edit attribute actions has to be globally available so that we can dispose of its dialog properly. */
+    private AddOrEditAttributeActionListener add_or_edit_attribute_action_listener = null;
+    /** The class used to handle add or edit value actions has to be globally available so that we can dispose of its dialog properly. */
+    private AddOrEditValueActionListener add_or_edit_value_action_listener = null;
+    private boolean ignore = false;
+    /** A card layout is used to switch between the two differing detail views. */ 
+    private CardLayout card_layout = null;
+    /** A card layout is used to switch between a value tree or an empty placeholder (if no value tree available). */
+    private CardLayout element_values_layout = null;
+    /** Um, the size of the screen I'd guess. */
+    private Dimension screen_size = null;
+    private ElementWrapper current_element = null;
+    private GValueNode current_value_node = null;
+    /** A reference to ourselves so our inner classes can dispose of us. */
+    private GEMS self = null;
+    private int current_attribute = -1;
+    private int current_attribute_type = -1;
+    private JButton add_attribute = null;
+    private JButton add_element = null;
+    private JButton add_file = null;
+    private JButton add_set = null;
+    private JButton add_value = null;
+    private JButton edit_attribute = null;
+    private JButton edit_value = null;
+    private JButton remove_attribute = null;
+    private JButton remove_element = null;
+    private JButton remove_file = null;
+    private JButton remove_set = null;
+    private JButton remove_value = null;
+    private JLabel element_name = null;
+    private JLabel profile_name = null;
+    private JLabel set_name = null;
+    private JScrollPane element_attributes_scroll = null;
+    private JScrollPane profile_attributes_scroll = null;
+    private JScrollPane set_attributes_scroll = null;
+    private JPanel details_pane = null;
+    private JPanel element_values_pane = null;
+    private KeepTreeRootExpandedListener keep_root_expanded_listener = null;
+    private GEMSModel model = null;
+    private GEMSNode current_node = null;
+    private MetadataSet current_set = null;
+    private Object target = null;
+    private RemoveSetActionListener remove_set_action_listener;
+    private SmarterTable element_attributes = null;
+    private SmarterTable profile_attributes = null;
+    private SmarterTable set_attributes = null;
+    private SmarterTree element_values = null;
+    /** A tree that represents the current metadata sets associated with this collection. */
+    private SmarterTree mds_tree = null;
+    private String current_collection_file = null;
+    private String dialog_options[] = null;
+
+
+    static public void main(String[] args)
+    {
+	// Parse arguments
+	GetOpt go = new GetOpt(args);
+
+	// Debugging control
+	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);
+	}
+
+	// Load configuration file
+	try {
+	    config = new Configuration(go.gsdl_path, null, go.exec_path, go.perl_path, false, null);
+	}
+	catch (Exception exception) {
+	    DebugStream.printStackTrace(exception);
+	}
+
+	new GEMS(null, NORMAL);
+    }
+
+
+    /** Constructor.
+     * @param set a MetadataSet that should be initially selected
+     * @param action a systematic action that should be performed
+     */
+    public GEMS(MetadataSet set, int action)
+    {
+	this.dialog_options = new String[2];
+	this.screen_size = config.screen_size;
+	this.self = this;
+
+	// Initialise some common images
+	Utility.initImages(this);
+
+	// Load help
+	HelpFrame help = new HelpFrame();
+
+	dialog_options[0] = Dictionary.get("General.OK");
+	dialog_options[1] = Dictionary.get("General.Cancel");
+
+	// Creation
+	setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+	setSize(SIZE);
+	setJMenuBar(new GEMSMenuBar());
+	Dictionary.setText(this, "MEM.Title");
+
+	JPanel content_pane = (JPanel) getContentPane();
+	content_pane.setBackground(config.getColor("coloring.collection_heading_background", false));
+		  
+	JPanel upper_pane = new JPanel();
+	upper_pane.setOpaque(false);
+		  
+	model = new GEMSModel();
+
+	JPanel mds_tree_pane = new JPanel();
+	mds_tree_pane.setOpaque(false);
+	mds_tree_pane.setPreferredSize(new Dimension(200,500));
+	mds_tree = new SmarterTree(model);
+	mds_tree.setCellRenderer(new MEMTreeCellRenderer());
+	mds_tree.setRootVisible(false);
+	mds_tree.setBackground(config.getColor("coloring.collection_tree_background", false));
+	mds_tree.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	mds_tree.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	mds_tree.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	// We may have been asked to select a cetain initial set
+	if(set != null) {
+	    GEMSNode root = (GEMSNode) model.getRoot();
+	    for(int i = 0; i < root.getChildCount(); i++) {
+		GEMSNode child = (GEMSNode) root.getChildAt(i);
+		if(set.equals(child.getUserObject())) {
+		    TreePath path = new TreePath(child.getPath());
+		    mds_tree.setSelectionPath(path);
+		    mds_tree.expandPath(path);
+		    path = null;
+		}
+		child = null;
+	    }
+	    root = null;
+	}
+
+	details_pane = new JPanel();
+	details_pane.setOpaque(false);
+	card_layout = new CardLayout();
+		  
+	JPanel set_details_pane = new JPanel();
+	set_details_pane.setOpaque(false);
+		  
+	JPanel set_name_pane = new JPanel();
+	set_name_pane.setOpaque(false);
+	JLabel set_name_label = new JLabel();
+	set_name_label.setOpaque(false);
+	Dictionary.setText(set_name_label, "MEM.Name");
+	set_name = new JLabel();
+	set_name.setBorder(BorderFactory.createLoweredBevelBorder());
+	set_name.setOpaque(false);
+		  
+	JPanel set_attributes_pane = new JPanel();
+	set_attributes_pane.setOpaque(false);
+	set_attributes_scroll = new JScrollPane();
+	set_attributes_scroll.getViewport().setBackground(config.getColor("coloring.collection_tree_background", false));
+	set_attributes_scroll.setOpaque(true);
+	set_attributes = new SmarterTable();
+	set_attributes.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+	set_attributes.setBackground(config.getColor("coloring.collection_tree_background", false));
+	set_attributes.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	set_attributes.setHeadingBackground(config.getColor("coloring.collection_heading_background", false));
+	set_attributes.setHeadingForeground(config.getColor("coloring.collection_heading_foreground", false));
+	set_attributes.setOpaque(false);
+	set_attributes.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	set_attributes.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	set_attributes.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+		  
+	JPanel element_details_pane = new JPanel();
+	element_details_pane.setOpaque(false);
+
+	JPanel element_name_pane = new JPanel();
+	element_name_pane.setOpaque(false);
+	JLabel element_name_label = new JLabel();
+	element_name_label.setOpaque(false);
+	Dictionary.setText(element_name_label, "MEM.Name");
+	element_name = new JLabel();
+	element_name.setBorder(BorderFactory.createLoweredBevelBorder());
+	element_name.setOpaque(false);
+		  
+	JPanel element_inner_pane = new JPanel();
+	element_inner_pane.setOpaque(false);
+		  
+	JPanel element_attributes_pane = new JPanel();
+	element_attributes_pane.setOpaque(false);
+	element_attributes_scroll = new JScrollPane();
+	element_attributes_scroll.getViewport().setBackground(config.getColor("coloring.collection_tree_background", false));
+	element_attributes_scroll.setOpaque(true);
+	element_attributes = new SmarterTable();
+	element_attributes.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+	element_attributes.setBackground(config.getColor("coloring.collection_tree_background", false));
+	element_attributes.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	element_attributes.setHeadingBackground(config.getColor("coloring.collection_heading_background", false));
+	element_attributes.setHeadingForeground(config.getColor("coloring.collection_heading_foreground", false));
+	element_attributes.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	element_attributes.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	element_attributes.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+	element_values_layout = new CardLayout();
+	element_values_pane = new JPanel();
+	element_values_pane.setOpaque(false);
+	element_values = new SmarterTree();
+	element_values.setBackground(config.getColor("coloring.collection_tree_background", false));
+	element_values.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	element_values.setRootVisible(false);
+	element_values.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	element_values.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+
+	JPanel element_novalues_pane = new JPanel();
+
+	keep_root_expanded_listener = new KeepTreeRootExpandedListener();
+
+	JPanel profile_details_pane = new JPanel();
+	profile_details_pane.setOpaque(false);
+	JPanel profile_name_pane = new JPanel();
+	profile_name_pane.setOpaque(false);
+	JLabel profile_name_label = new JLabel();
+	profile_name_label.setOpaque(false);
+	Dictionary.setText(profile_name_label, "MEM.Name");
+	profile_name = new JLabel();
+	profile_name.setBorder(BorderFactory.createLoweredBevelBorder());
+	profile_name.setOpaque(false);
+
+	JPanel profile_attributes_pane = new JPanel();
+	profile_attributes_pane.setOpaque(false);
+	profile_attributes_scroll = new JScrollPane();
+	profile_attributes_scroll.getViewport().setBackground(config.getColor("coloring.collection_tree_background", false));
+	profile_attributes_scroll.setOpaque(true);
+	profile_attributes = new SmarterTable();
+	profile_attributes.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+	profile_attributes.setBackground(config.getColor("coloring.collection_tree_background", false));
+	profile_attributes.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	profile_attributes.setHeadingBackground(config.getColor("coloring.collection_heading_background", false));
+	profile_attributes.setHeadingForeground(config.getColor("coloring.collection_heading_foreground", false));
+	profile_attributes.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	profile_attributes.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	profile_attributes.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+	JPanel button_pane = new JPanel();
+	button_pane.setOpaque(false);
+
+	JPanel button_label_pane = new JPanel();
+	button_label_pane.setOpaque(false);
+
+	JLabel attribute_label = new JLabel();
+	attribute_label.setOpaque(false);
+	Dictionary.setText(attribute_label, "MEM.Attribute");
+
+	JLabel element_label = new JLabel();
+	element_label.setOpaque(false);
+	Dictionary.setText(element_label, "MEM.Element");
+
+	JLabel file_label = new JLabel();
+	file_label.setOpaque(false);
+	Dictionary.setText(file_label, "MEM.File");
+
+	JLabel set_label = new JLabel();
+	set_label.setOpaque(false);
+	Dictionary.setText(set_label, "MEM.Set");
+
+	JLabel value_label = new JLabel();
+	value_label.setOpaque(false);
+	Dictionary.setText(value_label, "MEM.Value");
+
+	JPanel inner_button_pane = new JPanel();
+	inner_button_pane.setOpaque(false);
+	add_attribute = new GLIButton();
+	add_attribute.setMnemonic(KeyEvent.VK_4);
+	Dictionary.setBoth(add_attribute, "MEM.Add", "MEM.Add_Attribute_Tooltip");
+	add_element = new GLIButton();
+	add_element.setMnemonic(KeyEvent.VK_3);
+	Dictionary.setBoth(add_element, "MEM.Add", "MEM.Add_Element_Tooltip");
+	add_file = new GLIButton();
+	add_file.setMnemonic(KeyEvent.VK_2);
+	Dictionary.setBoth(add_file, "MEM.Add", "MEM.Add_File_Tooltip");
+	add_set = new GLIButton();
+	add_set.setMnemonic(KeyEvent.VK_1);
+	Dictionary.setBoth(add_set, "MEM.Add", "MEM.Add_Set_Tooltip");
+	add_value = new GLIButton();
+	add_value.setMnemonic(KeyEvent.VK_5);
+	Dictionary.setBoth(add_value, "MEM.Add", "MEM.Add_Value_Tooltip");
+
+	edit_attribute = new GLIButton();
+	edit_attribute.setMnemonic(KeyEvent.VK_6);
+	Dictionary.setBoth(edit_attribute, "MEM.Edit", "MEM.Edit_Attribute_Tooltip");
+	edit_value = new GLIButton();
+	edit_value.setMnemonic(KeyEvent.VK_7);
+	Dictionary.setBoth(edit_value, "MEM.Edit", "MEM.Edit_Value_Tooltip");
+
+	remove_attribute = new GLIButton();
+	remove_attribute.setMnemonic(KeyEvent.VK_MINUS);
+	Dictionary.setBoth(remove_attribute, "MEM.Remove", "MEM.Remove_Attribute_Tooltip");
+	remove_element = new GLIButton();
+	remove_element.setMnemonic(KeyEvent.VK_0);
+	Dictionary.setBoth(remove_element, "MEM.Remove", "MEM.Remove_Element_Tooltip");
+	remove_file = new GLIButton();
+	remove_file.setMnemonic(KeyEvent.VK_9);
+	Dictionary.setBoth(remove_file, "MEM.Remove", "MEM.Remove_File_Tooltip");
+	remove_set = new GLIButton();
+	remove_set.setMnemonic(KeyEvent.VK_8);
+	Dictionary.setBoth(remove_set, "MEM.Remove", "MEM.Remove_Set_Tooltip");
+	remove_value = new GLIButton();
+	remove_value.setMnemonic(KeyEvent.VK_EQUALS);
+	Dictionary.setBoth(remove_value, "MEM.Remove", "MEM.Remove_Value_Tooltip");
+	setControls(false, false, false, false, false, false, false, false, false, false, false, false);
+
+	add_element_action_listener = new AddElementActionListener();
+	add_file_action_listener = new AddFileActionListener();
+	add_set_action_listener = new AddSetActionListener();
+	add_or_edit_attribute_action_listener = new AddOrEditAttributeActionListener();
+	add_or_edit_value_action_listener = new AddOrEditValueActionListener();
+	remove_set_action_listener = new RemoveSetActionListener();
+
+	// Some blank panel
+	JPanel blank_pane = new JPanel();
+	blank_pane.setOpaque(false);
+	JPanel edit_file_pane = new JPanel();
+	edit_file_pane.setOpaque(false);
+	JPanel edit_element_pane = new JPanel();
+	edit_element_pane.setOpaque(false);
+	JPanel edit_set_pane = new JPanel();
+	edit_set_pane.setOpaque(false);
+
+	// Connection
+	add_attribute.addActionListener(add_or_edit_attribute_action_listener);
+	add_element.addActionListener(add_element_action_listener);
+	add_file.addActionListener(add_file_action_listener);
+	add_set.addActionListener(add_set_action_listener);
+	add_value.addActionListener(add_or_edit_value_action_listener);
+	edit_attribute.addActionListener(add_or_edit_attribute_action_listener);
+	edit_value.addActionListener(add_or_edit_value_action_listener);
+	remove_attribute.addActionListener(new RemoveAttributeActionListener());
+	remove_element.addActionListener(new RemoveElementActionListener());
+	remove_file.addActionListener(new RemoveFileActionListener());
+	remove_set.addActionListener(remove_set_action_listener);
+	remove_value.addActionListener(new RemoveValueActionListener());
+	element_attributes.getSelectionModel().addListSelectionListener(new AttributesListSelectionListener(element_attributes));
+	profile_attributes.getSelectionModel().addListSelectionListener(new AttributesListSelectionListener(profile_attributes));
+	set_attributes.getSelectionModel().addListSelectionListener(new AttributesListSelectionListener(set_attributes));
+	element_values.addTreeSelectionListener(new ElementValuesTreeSelectionListener());
+	mds_tree.addTreeSelectionListener(new MDSTreeSelectionListener());
+	// Layout
+	mds_tree_pane.setLayout(new BorderLayout());
+	mds_tree_pane.add(new JScrollPane(mds_tree), BorderLayout.CENTER);
+		  
+	set_name_pane.setLayout(new BorderLayout());
+	set_name_pane.add(set_name_label, BorderLayout.WEST);
+	set_name_pane.add(set_name, BorderLayout.CENTER);
+				
+	set_attributes_scroll.setViewportView(set_attributes);
+
+	set_attributes_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(Dictionary.get("MEM.Attributes")), BorderFactory.createEmptyBorder(2,2,2,2)));
+	set_attributes_pane.setLayout(new BorderLayout());
+	set_attributes_pane.add(set_attributes_scroll, BorderLayout.CENTER);
+		
+	set_details_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(Dictionary.get("MEM.Set_Details")), BorderFactory.createEmptyBorder(2,2,2,2)));
+	set_details_pane.setLayout(new BorderLayout());
+	//set_details_pane.add(set_name_pane, BorderLayout.NORTH);
+	set_details_pane.add(set_attributes_pane, BorderLayout.CENTER);
+
+	element_name_pane.setLayout(new BorderLayout());
+	element_name_pane.add(element_name_label, BorderLayout.WEST);
+	element_name_pane.add(element_name, BorderLayout.CENTER);
+				
+	element_attributes_scroll.setViewportView(element_attributes);
+
+	element_attributes_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(Dictionary.get("MEM.Attributes")), BorderFactory.createEmptyBorder(2,2,2,2)));
+	element_attributes_pane.setLayout(new BorderLayout());
+	element_attributes_pane.add(element_attributes_scroll, BorderLayout.CENTER);
+		  
+	element_values_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(Dictionary.get("MEM.Values")), BorderFactory.createEmptyBorder(2,2,2,2)));
+	//element_values_pane.setLayout(new BorderLayout());
+	//element_values_pane.add(new JScrollPane(element_values), BorderLayout.CENTER);
+	element_values_pane.setLayout(element_values_layout);
+	element_values_pane.add(element_novalues_pane, BLANK);
+	element_values_pane.add(new JScrollPane(element_values), VALUES);
+
+	element_inner_pane.setLayout(new GridLayout(2,1));
+	element_inner_pane.add(element_attributes_pane);
+	element_inner_pane.add(element_values_pane);
+		  
+	element_details_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(Dictionary.get("MEM.Element_Details")), BorderFactory.createEmptyBorder(2,2,2,2)));
+	element_details_pane.setLayout(new BorderLayout());
+	//element_details_pane.add(element_name_pane, BorderLayout.NORTH);
+	element_details_pane.add(element_inner_pane, BorderLayout.CENTER);
+
+	profile_name_pane.setLayout(new BorderLayout());
+	profile_name_pane.add(profile_name_label, BorderLayout.WEST);
+	profile_name_pane.add(profile_name, BorderLayout.CENTER);
+
+	profile_attributes_scroll.setViewportView(profile_attributes);
+
+	profile_attributes_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(Dictionary.get("MEM.Profiles")), BorderFactory.createEmptyBorder(2,2,2,2)));
+	profile_attributes_pane.setLayout(new BorderLayout());
+	profile_attributes_pane.add(profile_attributes_scroll, BorderLayout.CENTER);
+
+	profile_details_pane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(Dictionary.get("MEM.Profile_Details")), BorderFactory.createEmptyBorder(2,2,2,2)));
+	profile_details_pane.setLayout(new BorderLayout());
+	//profile_details_pane.add(profile_name_pane, BorderLayout.NORTH);
+	profile_details_pane.add(profile_attributes_pane, BorderLayout.CENTER);
+
+	details_pane.setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
+	details_pane.setLayout(card_layout);
+	details_pane.add(blank_pane, BLANK);
+	details_pane.add(set_details_pane, SET);
+	details_pane.add(element_details_pane, ELEMENT);
+	details_pane.add(profile_details_pane, PROFILE);
+
+	upper_pane.setLayout(new BorderLayout());
+	upper_pane.add(mds_tree_pane, BorderLayout.WEST);
+	upper_pane.add(details_pane, BorderLayout.CENTER);
+		  
+	button_label_pane.setLayout(new GridLayout(4,1,0,2));
+	button_label_pane.setBorder(BorderFactory.createEmptyBorder(0,0,0,2));
+	button_label_pane.add(set_label);
+	// button_label_pane.add(file_label);
+	button_label_pane.add(element_label);
+	button_label_pane.add(attribute_label);
+	button_label_pane.add(value_label);
+
+	inner_button_pane.setLayout(new GridLayout(4,3,0,2));		
+	inner_button_pane.add(add_set);
+	inner_button_pane.add(edit_set_pane);
+	inner_button_pane.add(remove_set);
+	// inner_button_pane.add(add_file);
+	// inner_button_pane.add(edit_file_pane);
+	// inner_button_pane.add(remove_file);
+	inner_button_pane.add(add_element);
+	inner_button_pane.add(edit_element_pane);
+	inner_button_pane.add(remove_element);
+	inner_button_pane.add(add_attribute);
+	inner_button_pane.add(edit_attribute);
+	inner_button_pane.add(remove_attribute);
+	inner_button_pane.add(add_value);
+	inner_button_pane.add(edit_value);
+	inner_button_pane.add(remove_value);
+		  
+	button_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
+	button_pane.setLayout(new BorderLayout());
+	button_pane.add(button_label_pane, BorderLayout.WEST);
+	button_pane.add(inner_button_pane, BorderLayout.CENTER);
+
+	content_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	content_pane.setLayout(new BorderLayout());
+	content_pane.add(upper_pane, BorderLayout.CENTER);
+	content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	// Callback - we may have been asked to perform some systematic action, but we can't do that until the dialog is visible, which in itself causes a problem as the dialog is modal and setVisible won't return until the dialog is cancelled. The solution is to spawn a task on a new thread which waits until the dialog is visible then calls doClick on the appropriate button.
+	if(action != NORMAL) {
+	    ActionTask task = new ActionTask(action);
+	    task.start();
+	}
+	// Display
+	setLocation((screen_size.width - SIZE.width) / 2, (screen_size.height - SIZE.height) / 2);
+	setVisible(true);
+    }
+
+
+    private void exit()
+    {
+	// Flush debug
+	DebugStream.closeDebugStream();
+
+	System.exit(0);
+    }
+
+
+    private class GEMSMenuBar
+	extends JMenuBar
+	implements ActionListener
+    {
+	private JMenu file = null;
+	private JMenu edit = null;
+	private JMenu help = null;
+
+	public JMenuItem file_exit   = null;
+	public JMenuItem file_new    = null;
+	public JMenuItem file_open   = 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 GEMSMenuBar()
+	{
+	    file = new JMenu();
+	    file.setMnemonic(KeyEvent.VK_F);
+	    Dictionary.registerText(file, "Menu.File");
+
+	    file_exit = new JMenuItem();
+	    file_exit.addActionListener(this);
+	    file_exit.setMnemonic(KeyEvent.VK_X);
+	    Dictionary.registerText(file_exit, "Menu.File_Exit");
+
+	    file_new = new JMenuItem();
+	    file_new.addActionListener(this);
+	    file_new.setMnemonic(KeyEvent.VK_N);
+	    Dictionary.registerText(file_new, "Menu.File_New");
+
+	    file_open = new JMenuItem();
+	    file_open.addActionListener(this);
+	    file_open.setMnemonic(KeyEvent.VK_O);
+	    Dictionary.registerText(file_open, "Menu.File_Open");
+
+	    file_save = new JMenuItem();
+	    file_save.addActionListener(this);
+	    file_save.setMnemonic(KeyEvent.VK_S);
+	    Dictionary.registerText(file_save, "Menu.File_Save");
+
+	    // Layout (file menu)
+	    file.add(file_new);
+	    file.add(file_open);
+	    file.add(file_save);
+	    file.add(new JSeparator());
+	    file.add(file_exit);
+
+	    // Edit menu
+	    edit = new JMenu();
+	    edit.setMnemonic(KeyEvent.VK_E);
+	    Dictionary.registerText(edit, "Menu.Edit");
+
+	    edit_cut = new JMenuItem();
+	    edit_cut.addActionListener(this);
+	    edit_cut.setMnemonic(KeyEvent.VK_X);
+	    Dictionary.registerText(edit_cut, "Menu.Edit_Cut");
+
+	    edit_copy = new JMenuItem();
+	    edit_copy.addActionListener(this);
+	    edit_copy.setMnemonic(KeyEvent.VK_C);
+	    Dictionary.registerText(edit_copy, "Menu.Edit_Copy");
+
+	    edit_paste = new JMenuItem();
+	    edit_paste.addActionListener(this);
+	    edit_paste.setMnemonic(KeyEvent.VK_V);
+	    Dictionary.registerText(edit_paste, "Menu.Edit_Paste");
+
+	    // Layout (edit menu)
+	    edit.add(edit_cut);
+	    edit.add(edit_copy);
+	    edit.add(edit_paste);
+
+	    // Help menu
+	    help = new JMenu();
+	    help.setIcon(Utility.HELP_ICON);
+	    Dictionary.setText(help, "Menu.Help");
+
+	    help_help = new JMenuItem();
+	    help_help.addActionListener(this);
+	    Dictionary.registerText(help_help, "Menu.Help");
+
+	    // Layout (help menu)
+	    help.add(help_help);
+
+	    // 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) {
+		add_set.doClick();
+		return;
+	    }
+
+	    // File -> Open
+	    if (event_source == file_open) {
+		JFileChooser metadata_set_file_chooser = new JFileChooser(new File(Utility.METADATA_DIR));
+		metadata_set_file_chooser.setFileFilter(new MetadataSetFileFilter());
+		if (metadata_set_file_chooser.showDialog(this, Dictionary.get("General.Open")) == JFileChooser.APPROVE_OPTION) {
+		    MetadataSet metadata_set = msm.loadMetadataSet(metadata_set_file_chooser.getSelectedFile());
+		    // Update tree
+		    model.add(null, metadata_set, GEMSNode.SET);
+		}
+		return;
+	    }
+
+	    // File -> Save
+	    if (event_source == file_save) {
+		// Don't actually need to do anything
+		return;
+	    }
+
+	    // File -> Exit
+	    if (event_source == file_exit) {
+		exit();
+	    }
+
+	    // 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;
+	    }
+
+	    // Help -> Help
+	    if (event_source == help_help) {
+		HelpFrame.setView("editingmetadatasets");
+		return;
+	    }
+	}
+    }
+
+
+    private 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");
+	}
+    }
+
+
+    private class ActionTask
+	extends Thread {
+	private int action;
+	public ActionTask(int action) {
+	    this.action = action;
+	}
+	public void run() {
+	    boolean complete = false;
+	    while(!complete) {
+		if(self.isVisible()) {
+		    switch(action) {
+		    case ADD_SET:
+			add_set_action_listener.actionPerformed(new ActionEvent(this, 0, "Blarg!"));
+			break;
+		    case REMOVE_SET:
+			if(!mds_tree.isSelectionEmpty()) {
+			    remove_set_action_listener.actionPerformed(new ActionEvent(this, 0, "Blarg!"));
+			}
+			break;
+		    }
+		    complete = true;
+		}
+		else {
+		    try {
+			synchronized(this) {
+			    wait(100);
+			}
+		    }
+		    catch(Exception exception) {
+		    }
+		}
+	    }
+	}
+    }
+
+    public void dispose() {
+	// Destructor
+	card_layout = null;
+	screen_size = null;
+	self = null;
+	add_attribute = null;
+	add_element = null;
+	add_file = null;
+	add_set = null;
+	add_value = null;
+	edit_attribute = null;
+	edit_value = null;
+	remove_attribute = null;
+	remove_element = null;
+	remove_file = null;
+	remove_set = null;
+	remove_value = null;
+	details_pane = null;
+	element_attributes = null;
+	set_attributes = null;
+	element_name = null;
+	set_name = null;
+	element_values = null;
+	mds_tree = null;
+	target = null;
+
+	// Dispose of inner dialogs
+	if (add_element_action_listener != null) {
+	    add_element_action_listener.dispose();
+	    add_element_action_listener = null;
+	}
+	if (add_or_edit_attribute_action_listener != null) {
+	    add_or_edit_attribute_action_listener.dispose();
+	    add_or_edit_attribute_action_listener = null;
+	}
+	if (add_or_edit_value_action_listener != null) {
+	    add_or_edit_value_action_listener.dispose();
+	    add_or_edit_value_action_listener = null;
+	}
+	remove_set_action_listener = null;
+	super.dispose();
+    }
+
+	 
+    private void setControls(boolean a_s, boolean r_s, boolean a_f, boolean r_f, boolean a_e, boolean r_e, boolean a_a, boolean e_a, boolean r_a, boolean a_v, boolean e_v, boolean r_v) {
+	add_attribute.setEnabled(a_a);
+	add_element.setEnabled(a_e);
+	add_file.setEnabled(true); // Always true
+	add_set.setEnabled(true); // Always true
+	add_value.setEnabled(a_v);
+	edit_attribute.setEnabled(e_a);
+	edit_value.setEnabled(e_v);
+	remove_attribute.setEnabled(r_a);
+	remove_element.setEnabled(r_e);
+	remove_file.setEnabled(r_f);
+	remove_set.setEnabled(r_s);
+	remove_value.setEnabled(r_v);
+    }
+
+    private class AddOrEditAttributeActionListener
+	extends ModalDialog
+	implements ActionListener {
+	private boolean add_type = true;
+	private ComboArea value = null;
+	private JButton cancel_button = null;
+	private JButton ok_button = null;
+	private JComboBox language_box = null;
+	private GComboBox name = null;
+	private HashMap name_to_values = null;
+	private JLabel target = null;
+	/** Constructor. */
+	public AddOrEditAttributeActionListener() {
+	    super(self);
+	    setModal(true);
+	    setSize(ADD_OR_EDIT_ATTRIBUTE_SIZE);
+	    name_to_values = new HashMap();
+
+	    // Creation
+	    JPanel content_pane = (JPanel) getContentPane();
+	    content_pane.setBackground(config.getColor("coloring.collection_heading_background", false));
+	    JPanel upper_pane = new JPanel();
+	    upper_pane.setOpaque(false);
+
+	    JPanel labels_pane = new JPanel();
+	    JPanel boxes_pane = new JPanel();
+
+	    JLabel target_label = new JLabel();
+	    target_label.setOpaque(false);
+	    Dictionary.setText(target_label, "MEM.Target");
+	    target = new JLabel();
+	    target.setOpaque(false);
+
+	    JLabel name_label = new JLabel();
+	    name_label.setOpaque(false);
+	    Dictionary.setText(name_label, "MEM.Name");
+	    name = new GComboBox();
+	    name.setEditable(true);
+	    Dictionary.setTooltip(name, "MEM.Attribute_Name_Tooltip");
+
+	    JLabel language_label = new JLabel();
+	    language_label.setOpaque(false);
+	    Dictionary.setText(language_label, "MEM.Language");
+	    language_box = new JComboBox();  // !!! Gatherer.g_man.design_pane.getLanguageCodes().toArray());
+	    // !!! language_box.setRenderer(new LanguageListCellRenderer());
+	    Dictionary.setTooltip(language_box, "MEM.Attribute_Language_Tooltip");
+
+	    JPanel center_pane = new JPanel();
+	    center_pane.setOpaque(false);
+	    value = new ComboArea(Dictionary.get("MEM.Values"), COMPONENT_SIZE);
+	    value.setOpaque(false);
+
+	    JTextArea v_text_area = (JTextArea) value.getTextComponent();
+	    v_text_area.setBackground(config.getColor("coloring.collection_tree_background", false));
+	    v_text_area.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	    v_text_area.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	    v_text_area.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	    Dictionary.setTooltip(v_text_area, "MEM.Attribute_Value_Tooltip");
+
+	    JPanel button_pane = new JPanel();
+	    button_pane.setOpaque(false);
+	    ok_button = new GLIButton();
+	    ok_button.setMnemonic(KeyEvent.VK_O);
+	    Dictionary.setBoth(ok_button, "General.OK", "General.OK_Tooltip");
+	    cancel_button = new GLIButton();
+	    cancel_button.setMnemonic(KeyEvent.VK_C);
+	    Dictionary.setBoth(cancel_button, "General.Cancel", "General.Pure_Cancel_Tooltip");
+
+	    // Connection
+	    TextFieldEnabler ok_button_enabler = new TextFieldEnabler(ok_button);
+	    cancel_button.addActionListener(this);
+	    name.addActionListener(this);
+	    ok_button.addActionListener(this);
+	    ok_button_enabler.add((JTextComponent)name.getEditor()); // Assuming the default editor is a JTextField!
+
+	    // Layout
+	    labels_pane.setLayout(new GridLayout(3,1,0,5));
+	    labels_pane.add(target_label);
+	    labels_pane.add(name_label);
+	    labels_pane.add(language_label);
+
+	    boxes_pane.setLayout(new GridLayout(3,1,0,5));
+	    boxes_pane.add(target);
+	    boxes_pane.add(name);
+	    boxes_pane.add(language_box);
+	    
+	    upper_pane.setLayout(new BorderLayout(5,0));
+	    upper_pane.add(labels_pane, BorderLayout.WEST);
+	    upper_pane.add(boxes_pane, BorderLayout.CENTER);
+
+	    value.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+
+	    button_pane.setLayout(new GridLayout(1,2,5,0));
+	    button_pane.add(ok_button);
+	    button_pane.add(cancel_button);
+
+	    center_pane.setLayout(new BorderLayout());
+	    center_pane.add(value, BorderLayout.CENTER);
+	    center_pane.add(button_pane, BorderLayout.SOUTH);
+
+	    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);
+
+	    setLocation((config.screen_size.width - ADD_OR_EDIT_ATTRIBUTE_SIZE.width) / 2, (config.screen_size.height - ADD_OR_EDIT_ATTRIBUTE_SIZE.height) / 2);
+	}
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action as occured on our registered component, allowing us to add a new attribute to the selected set or element.
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    Object source = event.getSource();
+	    if(source == ok_button) {
+		boolean success = true;
+		AttributeTableModel model;
+
+		// Add or edit a collect/set/element attribute
+		switch(current_attribute_type) {
+		case GEMSNode.COLLECTION:
+		    model = (AttributeTableModel) profile_attributes.getModel();
+		    success = addOrEditCollectionAttribute(model);
+		    break;
+		case GEMSNode.SET:
+		    model = (AttributeTableModel) set_attributes.getModel();
+		    success = addOrEditSetAttribute(model);		    
+		    break;
+		case GEMSNode.ELEMENT:
+		    model = (AttributeTableModel) element_attributes.getModel();
+		    success = addOrEditElementAttribute(model);
+		    break;
+		}
+
+		if (success) {
+		    // Hide dialog
+		    setVisible(false);
+		}
+	    }
+	    else if(source == cancel_button) {
+		// Hide dialog
+		setVisible(false);
+	    }
+	    else if(source == name) {
+		Object object = name.getSelectedItem();
+		if(object != null) {
+		    java.util.List values = (java.util.List) name_to_values.get(object.toString());
+		    if(values == null && current_attribute_type==GEMSNode.COLLECTION) { 
+			values = msm.getElements();
+		    }
+		    value.clear();
+		    for(int i = 0; i < values.size(); i++) {
+			value.add(values.get(i));
+		    }
+		}
+	    }
+	    else {
+		// Name combo box is enabled except when attributes are edited
+		//name.setEnabled((source != edit_attribute));
+		name_to_values.clear();
+		name.clear();
+		value.clear();
+		value.setText("");
+		// Build the correct name_to_values mapping
+		// Attributes are slightly tricky as they can come from several sources. Has a collection file been selected...
+		if(current_collection_file != null) {
+		    target.setText(current_collection_file);
+		    // Name is empty in this one, however values must be the current elements in the collection.						  
+		    language_box.setEnabled(false);
+		}
+		// or has an element been selected
+		else if(current_element != null) {
+		    target.setText(current_element.toString());
+		    // Develop a model for name based on known attributes from all other elements.
+		    java.util.List elements = msm.getElements();
+		    for(int i = 0; i < elements.size(); i++) {
+			ElementWrapper element = (ElementWrapper) elements.get(i);
+			TreeSet attributes = element.getAttributes();
+			for(Iterator attribute_iterator = attributes.iterator(); attribute_iterator.hasNext(); ) {
+			    Attribute attribute = (Attribute) attribute_iterator.next();
+			    java.util.List values = (java.util.List) name_to_values.get(attribute.name);
+			    if(values == null) {
+				values = new ArrayList();
+				name_to_values.put(attribute.name, values);
+			    }
+			    if(!values.contains(attribute.value)) {
+				values.add(attribute.value);
+			    }
+			    values = null;
+			    attribute = null;
+			}
+			attributes = null;
+			element = null;
+		    }
+		    elements = null;
+		    language_box.setEnabled(true);
+		}
+		else if(current_set != null) {
+		    target.setText(current_set.toString());
+		    // Develop a model for name based on known attributes from all other metadata sets. At the same time build a hashmap mapping attribute name to lists of values.
+		    java.util.List sets = msm.getSets();
+		    for(int i = 0; i < sets.size(); i++) {
+			MetadataSet set = (MetadataSet) sets.get(i);
+			NamedNodeMap attributes = set.getAttributes();
+			for(int j = 0; j < attributes.getLength(); j++) {
+			    Attr attribute = (Attr) attributes.item(j);
+			    String name_str = attribute.getName();
+			    String value_str = attribute.getValue();
+			    java.util.List values = (java.util.List) name_to_values.get(name_str);
+			    if(values == null) {
+				values = new ArrayList();
+				name_to_values.put(name_str, values);
+			    }
+			    if(!values.contains(value_str)) {
+				values.add(value_str);
+			    }
+			    values = null;
+			    value_str = null;
+			    name_str = null;
+			    attribute = null;
+			}
+			attributes = null;
+			set = null;
+			language_box.setEnabled(false);
+		    }
+		    sets = null;
+		    // If this is an add remove all the attributes already present in the current set.
+		    if(source == add_attribute) {
+			name.setEnabled(true);
+			NamedNodeMap attributes = current_set.getAttributes();
+			for(int i = 0; i < attributes.getLength(); i++) {
+			    Attr attribute = (Attr) attributes.item(i);
+			    String name_str = attribute.getName();
+			    name_to_values.remove(name_str);
+			    name_str = null;
+			    attribute = null;
+			}
+			attributes = null;
+		    }
+		}
+		// Otherwise we actually disable the name combobox
+		else {
+		    name.setEnabled(false);
+		}
+		// Now name_to_values should contain a list of unique attribute names each mapping to a list of attribute values.
+		for(Iterator name_iterator = name_to_values.keySet().iterator(); name_iterator.hasNext(); ) {
+		    name.add(name_iterator.next());
+		}
+		// Now pritty up the dialog depending on the action type
+		if(source == add_attribute) {
+		    add_type = true;
+		    Dictionary.setText(this, "MEM.AddAttribute");
+		    if(current_collection_file != null) {
+			// Name is empty in this one, however values must be the current elements in the collection.	
+			java.util.List values = msm.getElements();
+			for(int i = 0; i < values.size(); i++) {
+			    value.add(new NameElementWrapperEntry(values.get(i)));
+			}
+			values = null;
+		    }
+		    setVisible(true);
+		}
+		else if(current_attribute != -1) {
+		    AttributeTableModel model = null;
+		    switch(current_attribute_type) {
+		    case GEMSNode.COLLECTION:
+			model = (AttributeTableModel) profile_attributes.getModel();
+			break;
+		    case GEMSNode.ELEMENT:
+			model = (AttributeTableModel) element_attributes.getModel();
+			break;
+		    case GEMSNode.SET:
+			model = (AttributeTableModel) set_attributes.getModel();
+			break;
+		    }
+		    add_type = false;
+		    Dictionary.setText(this, "MEM.EditAttribute");
+		    String name_str = (String) model.getValueAt(current_attribute, 0);
+		    String value_str = (String) model.getValueAt(current_attribute, model.getColumnCount() - 1);
+		    model = null;
+		    // Retrieve the appropriate value model
+		    java.util.List values = (java.util.List) name_to_values.get(name_str);
+		    // Only possible for collection file selections.
+		    if(values == null) {
+			values = msm.getElements();
+		    }
+		    name.setSelectedItem(name_str);
+		    name_str = null;
+		    for(int i = 0; i < values.size(); i++) {
+			Object temp_value = values.get(i);
+			if(temp_value instanceof ElementWrapper) {
+			    value.add(new NameElementWrapperEntry(temp_value));
+			}
+			else {
+			    value.add(temp_value);
+			}
+		    }
+		    values = null;
+		    value.setSelectedItem(value_str);
+		    value_str = null;
+		    setVisible(true);
+		}
+	    }
+	    source = null;
+	}
+
+
+	private boolean addOrEditCollectionAttribute(AttributeTableModel model)
+	{
+	    String name_str = name.getSelectedItem().toString();
+	    String value_str = Codec.transform(Codec.transformUnicode(value.getText()), Codec.TEXT_TO_DOM);
+
+	    // Remove the existing attribute if this is an edit
+	    if(!add_type && current_attribute != -1) {
+		String old_source = (String) model.getValueAt(current_attribute, 0);
+		// !!! msm.profiler.removeAction(current_collection_file, old_source);
+		old_source = null;
+		model.removeRow(current_attribute);
+	    }
+
+	    boolean cont = true;
+	    // Check that there isn't already an entry for this attribute
+	    if (!model.contains(name_str, 0)) {
+		// Add profile
+		// !!! msm.profiler.addAction(current_collection_file, name_str, value_str);
+		// Update attribute table
+		model.add(new Attribute(name_str, value_str));
+	    }
+	    // Otherwise show an error message and do not proceed
+	    else {
+		cont = false;
+		JOptionPane.showMessageDialog(self, Dictionary.get("MEM.Attribute_Already_Exists"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+	    }
+
+	    value_str = null;
+	    name_str = null;
+	    return cont;
+	}
+
+
+	private boolean addOrEditSetAttribute(AttributeTableModel model)
+	{
+	    String name_str = name.getSelectedItem().toString();
+	    String value_str = Codec.transform(Codec.transformUnicode(value.getText()), Codec.TEXT_TO_DOM);
+
+	    // Remove the existing attribute if this is an edit
+	    if(!add_type && current_attribute != -1) {
+		current_set.removeAttribute(name_str);
+		// Update attribute table
+		model.removeRow(current_attribute);
+	    }
+
+	    boolean cont = true;
+	    // Check that there isn't already an entry for this attribute
+  	    if (!model.contains(name_str, 0)) {
+ 		// Add the new attribute
+ 		current_set.addAttribute(name_str, value_str);
+ 		// Update the attribute table
+ 		model.add(new Attribute(name_str, value_str));
+ 	    }
+ 	    // Otherwise show an error message and do not proceed
+ 	    else {
+ 		cont = false;
+ 		JOptionPane.showMessageDialog(self, Dictionary.get("MEM.Attribute_Already_Exists"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+ 	    }
+
+	    value_str = null;
+	    name_str = null;
+	    return cont;
+	}
+
+
+	private boolean addOrEditElementAttribute(AttributeTableModel model)
+	{
+	    // Add the attribute, even if one of the same name already exists. Maybe one day someone would like to enforce the occurance rules but not me and not today.
+	    String name_str = name.getSelectedItem().toString();
+	    String language_code = (String) language_box.getSelectedItem();
+	    String value_str = Codec.transform(Codec.transformUnicode(value.getText()), Codec.TEXT_TO_DOM);
+
+	    // Remove the existing attribute if this is an edit
+	    if(!add_type && current_attribute != -1) {
+		Attribute old_attribute = model.getAttribute(current_attribute);
+		String old_value_str = old_attribute.value;
+		String old_lang_code = old_attribute.language;
+		current_element.removeAttribute(name_str, old_lang_code, old_value_str);
+		// Update the attribute table
+		model.removeRow(current_attribute);
+	    }
+
+	    // Add the new attribute
+	    current_element.addAttribute(name_str, language_code, value_str);
+	    // Update the attribute table
+	    int row = model.add(new Attribute(name_str, language_code, value_str));
+	    element_attributes.setRowSelectionInterval(row, row);
+
+	    value_str = null;
+	    language_code = null;
+	    name_str = null;
+	    return true;
+	}
+
+
+	public void dispose() {
+	    cancel_button = null;
+	    name = null;
+	    name_to_values = null;
+	    ok_button = null;
+	    target = null;
+	    value = null;
+	    ///ystem.err.println("Dispose AddOrEditAttributeActionListener");
+	    super.dispose();
+	}
+    }
+	 
+    private class AddElementActionListener
+	extends ModalDialog
+	implements ActionListener {
+	private JButton cancel_button = null;
+	private JButton ok_button = null;
+	private JLabel set_field = null;
+	private NonWhitespaceField name_field = null;
+	public AddElementActionListener() {
+	    super(self);
+	    setModal(true);
+	    setSize(ADD_ELEMENT_SIZE);
+	    // Creation
+	    JPanel content_pane = (JPanel) getContentPane();
+	    content_pane.setBackground(config.getColor("coloring.collection_heading_background", false));
+	    JPanel center_pane = new JPanel();
+	    center_pane.setOpaque(false);
+
+	    JPanel set_pane = new JPanel();
+	    set_pane.setOpaque(false);
+	    JLabel set_label = new JLabel();
+	    set_label.setOpaque(false);
+	    Dictionary.setText(set_label, "MEM.Set");
+	    set_field = new JLabel();
+	    set_field.setOpaque(false);
+
+	    JPanel name_pane = new JPanel();
+	    name_pane.setOpaque(false);
+	    JLabel name_label = new JLabel();
+	    name_label.setOpaque(false);
+	    Dictionary.setText(name_label, "MEM.Name");
+	    name_field = new NonWhitespaceField();
+	    name_field.setBackground(config.getColor("coloring.collection_tree_background", false));
+	    name_field.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	    name_field.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	    name_field.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	    Dictionary.setTooltip(name_field, "MEM.Element_Name_Tooltip");
+
+	    JPanel button_pane = new JPanel();
+	    button_pane.setOpaque(false);
+
+	    ok_button = new GLIButton();
+	    ok_button.setMnemonic(KeyEvent.VK_O);
+	    Dictionary.setBoth(ok_button, "General.OK", "General.OK_Tooltip");
+	    cancel_button = new GLIButton();
+	    cancel_button.setMnemonic(KeyEvent.VK_C);
+	    Dictionary.setBoth(cancel_button, "General.Cancel", "General.Pure_Cancel_Tooltip");
+
+	    TextFieldEnabler ok_button_enabler = new TextFieldEnabler(ok_button);
+
+	    // Connection
+	    cancel_button.addActionListener(this);
+	    ok_button.addActionListener(this);
+	    ok_button_enabler.add(name_field);
+
+	    // Layout
+	    set_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+	    set_pane.setLayout(new BorderLayout());
+	    set_pane.add(set_label, BorderLayout.WEST);
+	    set_pane.add(set_field, BorderLayout.CENTER);
+
+	    name_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+	    name_pane.setLayout(new BorderLayout(5,0));
+	    name_pane.add(name_label, BorderLayout.WEST);
+	    name_pane.add(name_field, BorderLayout.CENTER);
+
+	    center_pane.setLayout(new GridLayout(2,1,0,0));
+	    center_pane.add(set_pane);
+	    center_pane.add(name_pane);
+
+	    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(center_pane, BorderLayout.CENTER);
+	    content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	    setLocation((config.screen_size.width - ADD_ELEMENT_SIZE.width) / 2, (config.screen_size.height - ADD_ELEMENT_SIZE.height) / 2);
+	}
+
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action as occured on our registered component, allowing us to
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    Object source = event.getSource();
+	    if(source == ok_button) {
+		// Add then dispose
+		String name_str = Codec.transform(Codec.transformUnicode(name_field.getText()), Codec.TEXT_TO_DOM);
+		// If this element doesn't already exist.
+		if(!current_set.containsElement(name_str)) {
+		    // Add it,
+		    String language_code = config.getLanguage();
+		    ElementWrapper element = current_set.addElement(name_str, language_code);
+		    // Then update the tree
+		    model.add(current_node, element, GEMSNode.ELEMENT);
+		    // Signal that the metadata set has changed
+		    // msm.fireSetChanged(current_set);
+
+		    // Done
+		    element = null;
+		    setVisible(false);
+		}
+		// Otherwise show an error message and do not proceed.
+		else {
+		    JOptionPane.showMessageDialog(self, Dictionary.get("MEM.Element_Already_Exists"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		}
+		name_str = null;
+	    }
+	    else if(source == cancel_button) {
+		// Dispose
+		setVisible(false);
+	    }
+	    else {
+		if(current_set != null) {
+		    // You can't manually add elements to the Greenstone metadata set.
+		    if(!current_set.getNamespace().equals("")) {
+			set_field.setText(current_set.toString());
+			name_field.setText("");
+			// Display
+			setVisible(true);
+		    }
+		    // Warn the user that they can't do that dave.
+		    else {
+			JOptionPane.showMessageDialog(self, Dictionary.get("MEM.Cannot_Add_Elements_To_Greenstone_MDS"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		    }
+		}
+	    }
+	    source = null;
+	}
+
+	public void dispose() {
+	    cancel_button = null;
+	    ok_button = null;
+	    name_field = null;
+	    set_field = null;
+	    ///ystem.err.println("Dispose AddElementActionListener");
+	    super.dispose();
+	}
+    }
+	 
+    private class AddFileActionListener
+	extends ModalDialog
+	implements ActionListener {
+	private JButton cancel_button = null;
+	private JButton ok_button = null;
+	private JTextField name_field = null;
+	public AddFileActionListener() {
+	    super(self);
+	    setModal(true);
+	    setSize(ADD_FILE_SIZE);
+	    // Creation
+	    JPanel content_pane = (JPanel) getContentPane();
+	    content_pane.setBackground(config.getColor("coloring.collection_heading_background", false));
+	    JPanel center_pane = new JPanel();
+	    center_pane.setOpaque(false);
+	    JPanel profile_pane = new JPanel();
+	    profile_pane.setOpaque(false);
+	    JLabel profile_label = new JLabel();
+	    profile_label.setOpaque(false);
+	    Dictionary.setText(profile_label, "MEM.Profile");
+
+	    JPanel name_pane = new JPanel();
+	    name_pane.setOpaque(false);
+	    JLabel name_label = new JLabel();
+	    name_label.setOpaque(false);
+	    Dictionary.setText(name_label, "MEM.Name");
+	    name_field = new JTextField();
+	    name_field.setBackground(config.getColor("coloring.collection_tree_background", false));
+	    name_field.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	    name_field.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	    name_field.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	    Dictionary.setTooltip(name_field, "MEM.Profile_Name_Tooltip"); 
+
+	    JPanel button_pane = new JPanel();
+	    button_pane.setOpaque(false);
+
+	    ok_button = new GLIButton();
+	    ok_button.setMnemonic(KeyEvent.VK_O);
+	    Dictionary.setBoth(ok_button, "General.OK", "General.OK_Tooltip");
+	    cancel_button = new GLIButton();
+	    cancel_button.setMnemonic(KeyEvent.VK_C);
+	    Dictionary.setBoth(cancel_button, "General.Cancel", "General.Pure_Cancel_Tooltip");
+
+	    TextFieldEnabler ok_button_enabler = new TextFieldEnabler(ok_button);
+
+	    // Connection
+	    cancel_button.addActionListener(this);
+	    ok_button.addActionListener(this);
+	    ok_button_enabler.add(name_field);
+
+	    // Layout
+	    profile_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
+	    profile_pane.setLayout(new BorderLayout());
+	    profile_pane.add(profile_label, BorderLayout.CENTER);
+
+	    name_pane.setBorder(BorderFactory.createEmptyBorder(0,0,5,5));
+	    name_pane.setLayout(new BorderLayout(5,0));
+	    name_pane.add(name_label, BorderLayout.WEST);
+	    name_pane.add(name_field, BorderLayout.CENTER);
+
+	    center_pane.setLayout(new GridLayout(2,1,0,0));
+	    center_pane.add(profile_pane);
+	    center_pane.add(name_pane);
+
+	    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(center_pane, BorderLayout.CENTER);
+	    content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	    setLocation((config.screen_size.width - ADD_FILE_SIZE.width) / 2, (config.screen_size.height - ADD_FILE_SIZE.height) / 2);
+	}
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action as occured on our registered component, allowing us to
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    Object source = event.getSource();
+	    if(source == ok_button) {
+		String name_str = Codec.transform(Codec.transformUnicode(name_field.getText()), Codec.TEXT_TO_DOM);
+		// Ensure that this source doesn't already exist.
+// 		if(!msm.profiler.containsSource(name_str)) {
+// 		    // Add source with empty hashmap of actions.
+// 		    msm.profiler.addSource(name_str);
+// 		    // Add to tree
+// 		    model.add(model.getProfileNode(), name_str, GEMSNode.COLLECTION);
+// 		    setVisible(false);
+// 		}
+		// Otherwise warn the user and don't hide the prompt.
+		// else {
+		JOptionPane.showMessageDialog(self, Dictionary.get("MEM.File_Already_Exists"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		// }
+		name_str = null;
+	    }
+	    else if(source == cancel_button) {
+		setVisible(false);
+	    }
+	    else {
+		name_field.setText("");
+		setVisible(true);
+	    }
+	    source = null;
+	}
+
+	public void dispose() {
+	    cancel_button = null;
+	    ok_button = null;
+	    name_field = null;
+	    ///ystem.err.println("Dispose AddFileActionListener");
+	    super.dispose();
+	}
+    }
+
+    private class AddSetActionListener
+	extends ModalDialog
+	implements ActionListener {
+	private JButton cancel_button = null;
+	private JButton ok_button = null;
+	private JTextField name_field = null;
+	private JTextField namespace_field = null;
+	public AddSetActionListener() {
+	    super(self);
+	    setModal(true);
+	    setSize(ADD_SET_SIZE);
+	    Dictionary.setText(this, "MEM.AddSet");
+
+	    // Creation
+	    JPanel content_pane = (JPanel) getContentPane();
+	    content_pane.setBackground(config.getColor("coloring.collection_heading_background", false));
+	    JPanel center_pane = new JPanel();
+	    center_pane.setOpaque(false);
+
+	    JPanel label_pane = new JPanel();
+	    JPanel boxes_pane = new JPanel();
+
+	    JLabel namespace_label = new JLabel();
+	    namespace_label.setOpaque(false);
+	    Dictionary.setText(namespace_label, "MEM.Namespace");
+	    namespace_field = TransformCharacterTextField.createNamespaceTextField();
+	    namespace_field.setBackground(config.getColor("coloring.collection_tree_background", false));
+	    namespace_field.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	    namespace_field.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	    namespace_field.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	    Dictionary.setTooltip(namespace_field, "MEM.Set_Namespace_Tooltip");
+
+	    JLabel name_label = new JLabel();
+	    name_label.setOpaque(false);
+	    Dictionary.setText(name_label, "MEM.Name");
+	    name_field = new JTextField();
+	    name_field.setBackground(config.getColor("coloring.collection_tree_background", false));
+	    name_field.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	    name_field.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	    name_field.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	    Dictionary.setTooltip(name_field, "MEM.Set_Name_Tooltip");
+
+	    JPanel button_pane = new JPanel();
+	    button_pane.setOpaque(false);
+
+	    ok_button = new GLIButton();
+	    ok_button.setMnemonic(KeyEvent.VK_O);
+	    Dictionary.setBoth(ok_button, "General.OK", "General.OK_Tooltip");
+	    cancel_button = new GLIButton();
+	    cancel_button.setMnemonic(KeyEvent.VK_C);
+	    Dictionary.setBoth(cancel_button, "General.Cancel", "General.Pure_Cancel_Tooltip");
+	    TextFieldEnabler ok_button_enabler = new TextFieldEnabler(ok_button);
+
+	    // Connection
+	    cancel_button.addActionListener(this);
+	    ok_button.addActionListener(this);
+	    ok_button_enabler.add(name_field);
+	    ok_button_enabler.add(namespace_field);
+
+	    // Layout
+	    label_pane.setLayout(new GridLayout(2,1,0,5));
+	    label_pane.add(name_label);
+	    label_pane.add(namespace_label);
+
+	    boxes_pane.setLayout(new GridLayout(2,1,0,5));
+	    boxes_pane.add(name_field);
+	    boxes_pane.add(namespace_field);
+
+	    center_pane.setLayout(new BorderLayout(5,0));
+	    center_pane.add(label_pane, BorderLayout.WEST);
+	    center_pane.add(boxes_pane, BorderLayout.CENTER);
+
+	    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(center_pane, BorderLayout.CENTER);
+	    content_pane.add(button_pane, BorderLayout.SOUTH);
+
+	    setLocation((config.screen_size.width - ADD_SET_SIZE.width) / 2, (config.screen_size.height - ADD_SET_SIZE.height) / 2);
+	}
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action as occured on our registered component, allowing us to
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    Object source = event.getSource();
+	    if(source == ok_button) {
+		String namespace_str = namespace_field.getText();
+		String name_str = Codec.transform(Codec.transformUnicode(name_field.getText()), Codec.TEXT_TO_DOM);
+		// Ensure the set doesn't already exist
+		if(msm.getSet(name_str) == null) {
+		    MetadataSet set = msm.addSet(namespace_str, name_str);
+		    // Update tree.
+		    model.add(null, set, GEMSNode.SET);
+		    // Done
+		    set = null;
+		    setVisible(false);
+		}
+		// Otherwise show a warning.
+		else {
+		    JOptionPane.showMessageDialog(self, Dictionary.get("MEM.Set_Already_Exists"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
+		}
+		name_str = null;
+		namespace_str = null;
+	    }
+	    else if(source == cancel_button) {
+		setVisible(false);
+	    }
+	    else {
+		name_field.setText("");
+		setVisible(true);
+	    }
+	}
+		  
+	public void dispose() {
+	    cancel_button = null;
+	    ok_button = null;
+	    name_field = null;
+	    ///ystem.err.println("Dispose AddSetActionListener");
+	    super.dispose();
+	}
+    }
+	 
+    private class AddOrEditValueActionListener
+	extends ModalDialog
+	implements ActionListener, TreeSelectionListener {
+	private boolean add_type = true;
+	private boolean ignore = false;
+	private GValueNode subject_node = null;
+	private JButton cancel_button = null;
+	private JButton ok_button = null;
+	private JTextArea value = null;
+	private SmarterTree subject_tree = null;
+
+	/** Constructor. */
+	public AddOrEditValueActionListener() {
+	    super(self);
+	    this.setModal(true);
+	    this.setSize(ADD_OR_EDIT_VALUE_SIZE);
+
+	    // Create
+	    JPanel content_pane = (JPanel) getContentPane();
+	    content_pane.setBackground(config.getColor("coloring.collection_heading_background", false));
+	    JPanel center_pane = new JPanel();
+	    center_pane.setOpaque(false);
+	    JPanel inner_pane = new JPanel();
+	    inner_pane.setOpaque(false);
+	    JPanel subject_tree_pane = new JPanel();
+	    subject_tree_pane.setOpaque(false);
+	    JLabel subject_tree_label = new JLabel();
+	    subject_tree_label.setOpaque(false);
+	    Dictionary.setText(subject_tree_label, "MEM.Subject");
+	    subject_tree = new SmarterTree();
+	    subject_tree.setBackground(config.getColor("coloring.collection_tree_background", false));
+	    subject_tree.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	    subject_tree.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	    subject_tree.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	    JPanel value_pane = new JPanel();
+	    value_pane.setOpaque(false);
+	    JLabel value_label = new JLabel();
+	    value_label.setOpaque(false);
+	    Dictionary.setText(value_label, "MEM.Value"); 
+	    value = new JTextArea();
+	    value.setBackground(config.getColor("coloring.collection_tree_background", false));
+	    value.setForeground(config.getColor("coloring.collection_tree_foreground", false));
+	    value.setSelectionColor(config.getColor("coloring.collection_selection_background", false));
+	    value.setSelectedTextColor(config.getColor("coloring.collection_selection_foreground", false));
+	    Dictionary.setTooltip(value, "MEM.Value_Tooltip");
+
+	    JPanel button_pane = new JPanel();
+	    button_pane.setOpaque(false);
+
+	    ok_button = new GLIButton();
+	    ok_button.setMnemonic(KeyEvent.VK_O);
+	    ok_button.setEnabled(false);
+	    Dictionary.setBoth(ok_button, "General.OK", "General.OK_Tooltip");
+	    cancel_button = new GLIButton();
+	    cancel_button.setMnemonic(KeyEvent.VK_C);
+	    Dictionary.setBoth(cancel_button, "General.Cancel", "General.Pure_Cancel_Tooltip");
+	    TextFieldEnabler ok_button_enabler = new TextFieldEnabler(ok_button);
+
+	    // Connect
+	    ok_button_enabler.add(value);
+	    cancel_button.addActionListener(this);
+	    ok_button.addActionListener(this);
+	    subject_tree.addTreeSelectionListener(this);
+
+	    // Layout
+	    subject_tree_pane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	    subject_tree_pane.setLayout(new BorderLayout());
+	    subject_tree_pane.add(subject_tree_label, BorderLayout.NORTH);
+	    subject_tree_pane.add(new JScrollPane(subject_tree), BorderLayout.CENTER);
+
+	    value_pane.setBorder(BorderFactory.createEmptyBorder(0,5,5,5));
+	    value_pane.setLayout(new BorderLayout());
+	    value_pane.add(value_label, BorderLayout.NORTH);
+	    value_pane.add(new JScrollPane(value), BorderLayout.CENTER);
+				
+	    inner_pane.setLayout(new GridLayout(2,1));
+	    inner_pane.add(subject_tree_pane);
+	    inner_pane.add(value_pane);
+
+	    button_pane.setBorder(BorderFactory.createEmptyBorder(0,5,5,5));
+	    button_pane.setLayout(new GridLayout(1,2,5,5));
+	    button_pane.add(ok_button);
+	    button_pane.add(cancel_button);
+
+	    content_pane.setLayout(new BorderLayout());
+	    content_pane.add(inner_pane, BorderLayout.CENTER);
+	    content_pane.add(button_pane, BorderLayout.SOUTH);
+	    setLocation((config.screen_size.width - ADD_OR_EDIT_VALUE_SIZE.width) / 2, (config.screen_size.height - ADD_OR_EDIT_VALUE_SIZE.height) / 2);
+	}
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action as occured on our registered component, allowing us to add a new metadata value to the assigned values tree.
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    Object source = event.getSource();
+	    if(source == ok_button) {
+		String value_str = Codec.transform(Codec.transformUnicode(value.getText()), Codec.TEXT_TO_DOM);
+		// Now we action as necessary
+		GValueModel model = msm.getValueTree(current_element);
+		if(add_type) {
+		    ///ystem.err.println("Add type - insert node: " + value.getText() + " into " + subject_node);
+		    // Add this value to the tree.
+		    model.addValue(value_str, subject_node);
+		}
+		else {
+		    // Rewrite this value in the DOM model. Note that this will automatically rewrite all assignments as they are live references.
+		    current_value_node.setValue(value_str);
+		    // Now insert the node into the currently selected subject, but only if the parent has changed.
+		    if(subject_node != current_value_node.getParent()) {
+			GValueNode old_subject_node = (GValueNode) current_value_node.getParent();
+			// Find the new values position in subject_nodes children.
+			GValueNode sibling = null;
+			int index = 0;
+			boolean found = false;
+			while(index < subject_node.getChildCount() && !found) {
+			    sibling = (GValueNode) subject_node.getChildAt(index);
+			    int order = current_value_node.toString().compareToIgnoreCase(sibling.toString());
+			    // If the sibling is 'greater than' or comes after current value then insert.
+			    if(order < 0) {
+				// Insert. This will coincidently remove from original parent.
+				Node parent_node = subject_node.getElement();
+				Node child_node = current_value_node.getElement();
+				Node sibling_node = sibling.getElement();
+				parent_node.insertBefore(child_node, sibling_node);
+				found = true;
+			    }
+			    // The value already exists exactly as is. In theory this case can never happenm but just incase do nothing more.
+			    else if(order == 0) {
+				found = true;
+			    }
+			    // The sibling is 'less than' or before the current value, keep looking.
+			    else {
+				index++;
+			    }
+			}
+			// If we haven't done so yet, insert the current node. This will coincidently remove from original parent.
+			if(!found) {
+			    Node parent_node = subject_node.getElement();
+			    Node child_node = current_value_node.getElement();
+			    parent_node.appendChild(child_node);
+			}
+			// Inform the tree model what two nodes structures have changed (origin and destination).
+			//subject_node.unmap();
+			//old_subject_node.unmap();
+			model.nodeStructureChanged(old_subject_node);
+			model.nodeStructureChanged(subject_node);
+		    }
+		    // And if a data change was made tell the tree its data model is ka-bluey.
+		    else {
+			model.nodeChanged(current_value_node);
+		    }
+		}
+		model = null;
+		// Hide dialog
+		setVisible(false);
+
+	    }
+	    else if(source == cancel_button) {
+		// Hide dialog
+		setVisible(false);
+
+	    }
+	    else {
+		// Reset dialog
+		// current_value_node
+		GValueModel model = msm.getValueTree(current_element);
+		subject_tree.setModel(model);
+		// Task specific
+		if(source == add_value) {
+		    add_type = true;
+		    if(current_value_node != null) {
+			subject_node = current_value_node;
+		    }
+		    else {
+			subject_node = (GValueNode) model.getRoot();
+		    }
+		    value.setText("");
+		    Dictionary.setText(this, "MEM.AddValue");
+		}
+		else {
+		    add_type = false;
+		    if(current_value_node != null) {
+			subject_node = (GValueNode) current_value_node.getParent();
+		    }
+		    else {
+			subject_node = (GValueNode) model.getRoot();
+		    }
+		    value.setText(Codec.transform(current_value_node.toString(), Codec.DOM_TO_TEXT));
+		    Dictionary.setText(this, "MEM.EditValue");
+		}
+		model = null;
+		if(subject_node != null) {
+		    TreePath path = new TreePath(subject_node.getPath());
+		    subject_tree.scrollPathToVisible(path);
+		    subject_tree.setSelectionPath(path);
+		    path = null;
+		}
+		// Display
+		setVisible(true);
+	    }
+	}
+
+	public void dispose() {
+	    cancel_button = null;
+	    ok_button = null;
+	    subject_node = null;
+	    subject_tree = null;
+	    value = null;
+	    ///ystem.err.println("Dispose AddOrEditValueActionListener");
+	    super.dispose();
+	}
+
+	public void valueChanged(TreeSelectionEvent event) {
+	    if(subject_tree.getSelectionCount() > 0 && !ignore) {
+		ignore = true;
+		TreePath selected_path = subject_tree.getSelectionPath();
+		GValueNode requested_node = (GValueNode) selected_path.getLastPathComponent();
+		// Ensure the requested node is not a descendant of the current_value_node
+		if(current_value_node != null) {
+		    if(!add_type && current_value_node.isNodeDescendant(requested_node)) {
+			TreePath path = new TreePath(subject_node.getPath());
+			subject_tree.scrollPathToVisible(path);
+			subject_tree.setSelectionPath(path);
+		    }
+		    else {
+			subject_node = requested_node;
+		    }
+		}
+		selected_path = null;
+		ignore = false;
+	    }
+	}
+    }
+	 
+
+    /** This listener is responsible for keeping the root node of a value tree expanded where and when possible. */
+    private class KeepTreeRootExpandedListener
+	implements TreeModelListener {
+	/** Invoked after a node (or a set of siblings) has changed in some way. */
+	public void treeNodesChanged(TreeModelEvent e) {
+	}
+        /** Invoked after nodes have been inserted into the tree. */
+	public void treeNodesInserted(TreeModelEvent e) {
+	    if(element_values != null && element_values.getModel() != null && element_values.getModel().getRoot() != null) {
+		element_values.expandPath(new TreePath(element_values.getModel().getRoot()));
+	    }
+	}
+	/** Invoked after nodes have been removed from the tree. */
+	public void treeNodesRemoved(TreeModelEvent e) {
+	}
+	/** Invoked after the tree has drastically changed structure from a given node down. */
+	public void treeStructureChanged(TreeModelEvent e) {
+	    element_values.expandPath(new TreePath(element_values.getModel().getRoot()));
+	} 
+    }
+
+    private class RemoveAttributeActionListener
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action as occured on our registered component, allowing us to
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    if(current_attribute != -1) {
+		int result = JOptionPane.showOptionDialog(self, Dictionary.get("MEM.Confirm_Removal", Dictionary.get("MEM.Attribute")), Dictionary.get("MEM.Confirm_Removal_Title"), JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, dialog_options, dialog_options[0]);
+		// Remove this attribute
+		if(result == 0) {
+		    ignore = true;
+		    // Attributes are tricky little beasties, as they can come from several different places and depend upon what other little beasties have been selected
+		    // Has a collection file been selected...
+		    if(current_collection_file != null) {
+			// Remove a profile
+			String source = (String) profile_attributes.getValueAt(current_attribute, 0);
+			// !!! msm.profiler.removeAction(current_collection_file, source);
+			source = null;
+			// Refresh table
+			((AttributeTableModel)profile_attributes.getModel()).removeRow(current_attribute);
+		    }
+		    // or has an element been selected
+		    else if(current_element != null) {
+			// Remove element attribute
+			String name = (String) element_attributes.getValueAt(current_attribute, 0);
+			String language = (String) element_attributes.getValueAt(current_attribute, 1);
+			String value = (String) element_attributes.getValueAt(current_attribute, 2);
+			current_element.removeAttribute(name, language, value);
+
+			// Refresh table
+			((AttributeTableModel)element_attributes.getModel()).removeRow(current_attribute);
+		    }
+		    else if(current_set != null) {
+			String name = (String) set_attributes.getValueAt(current_attribute, 0);
+			// Remove set attribute
+			current_set.removeAttribute(name);
+			// Refresh table
+			((AttributeTableModel)set_attributes.getModel()).removeRow(current_attribute);
+		    }
+		    // Disable buttons
+		    edit_attribute.setEnabled(false);
+		    remove_attribute.setEnabled(false);
+		    ignore = false;
+		}
+	    }
+	}
+    }
+
+    private class RemoveElementActionListener
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action as occured on our registered component, allowing us to provide an editing prompt.
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    if(current_element != null) {
+		int result = JOptionPane.showOptionDialog(self, Dictionary.get("MEM.Confirm_Removal", Dictionary.get("MEM.Element")), Dictionary.get("MEM.Confirm_Removal_Title"), JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, dialog_options, dialog_options[0]);
+		// Remove this attribute
+		if(result == 0) {
+		    ignore = true;
+		    msm.removeElement(current_element);
+		    // Clear selection
+		    mds_tree.clearSelection();
+		    model.remove(current_element.toString(), GEMSNode.ELEMENT);
+		    // Meanwhile disable/enable controls given we had an element selected, but no longer do.
+		    remove_element.setEnabled(false);
+		    // Show a blank panel.
+		    card_layout.show(details_pane, BLANK);
+		    ignore = false;
+		}
+	    }
+	    else {
+		///ystem.err.println("No current element selected.");
+	    }
+	}
+    }
+
+    private class RemoveFileActionListener
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action as occured on our registered component, allowing us to provide an editing prompt.
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    if(current_collection_file != null) {
+		int result = JOptionPane.showOptionDialog(self, Dictionary.get("MEM.Confirm_Removal", Dictionary.get("MEM.File")), Dictionary.get("MEM.Confirm_Removal_Title"), JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, dialog_options, dialog_options[0]);
+		// Remove the current collection file profile from the profiler.
+		if(result == 0) {
+		    ignore = true;
+		    // !!! msm.profiler.removeProfile(current_collection_file);
+		    // Clear selection
+		    mds_tree.clearSelection();
+		    model.remove(current_collection_file, GEMSNode.COLLECTION);
+		    // Meanwhile disable/enable controls given we had a set selected, but no longer do.
+		    remove_file.setEnabled(false);
+		    // Show a blank panel.
+		    card_layout.show(details_pane, BLANK);
+		    ignore = false;
+		}
+	    }
+	}
+    }
+
+    private class RemoveSetActionListener
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action as occured on our registered component, allowing us to
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    if(current_set != null) {
+		int result = JOptionPane.showOptionDialog(self, Dictionary.get("MEM.Confirm_Removal", Dictionary.get("MEM.Set")), Dictionary.get("MEM.Confirm_Removal_Title"), JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, dialog_options, dialog_options[0]);
+		// Remove the currently selected set
+		if(result == 0) {
+		    ignore = true;
+		    msm.removeSet(current_set);
+		    // Clear selection
+		    mds_tree.clearSelection();
+		    model.remove(current_set.toString(), GEMSNode.SET);
+		    // Meanwhile disable/enable controls given we had a set selected, but no longer do.
+		    remove_set.setEnabled(false);
+		    // Show a blank panel.
+		    card_layout.show(details_pane, BLANK);
+		    ignore = false;
+		}
+	    }
+	}
+    }
+    /** This class will remove the currently selected metadata or profile value when any registered control is actioned. Note that removing a value acts differently from the other removes in that if you remove a value which is still assigned somewhere the values are restored next time said assignment is viewed. This turned out far easier and reasonable to code than attempting to remove all remaining values when you delete its reference in the GValueModel. */
+    private class RemoveValueActionListener
+	implements ActionListener {
+	/** Any implementation of ActionListener must include this method so that we can be informed when an action as occured on our registered component, allowing us to
+	 * @param event An <strong>ActionEvent</strong> containing information about the event.
+	 */
+	public void actionPerformed(ActionEvent event) {
+	    if(current_value_node != null) {				 
+		int result = JOptionPane.showOptionDialog(self, Dictionary.get("MEM.Confirm_Removal", Dictionary.get("MEM.Value")), Dictionary.get("MEM.Confirm_Removal_Title"), JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, dialog_options, dialog_options[0]);
+		// Remove the current selected value
+		if(result == 0) {
+		    ignore = true;
+		    GValueModel model = (GValueModel) element_values.getModel();
+		    model.removeValue(current_value_node);
+		    // Meanwhile disable/enable controls given we had a value selected, but no longer do.
+		    edit_value.setEnabled(false);
+		    remove_value.setEnabled(false);
+		    ignore = false;
+		}
+	    }
+	}
+    }
+	 
+    /** Used to enable a certain target component if and only if all the registered text fields contain non-zero length strings. */
+    private class TextFieldEnabler
+	implements DocumentListener {
+	private boolean ignore = false;
+	private Component target = null;
+	private JTextComponent[] fields = null;
+	public TextFieldEnabler(Component target) {
+	    super();
+	    this.target = target;
+	}
+	public void add(JTextComponent field) {
+	    // Record this field as being one we depend upon
+	    if(fields == null) {
+		fields = new JTextComponent[1];
+		fields[0] = field;
+	    }
+	    else {
+		JTextComponent[] temp = new JTextComponent[fields.length + 1];
+		System.arraycopy(fields, 0, temp, 0, fields.length);
+		temp[fields.length] = field;
+		fields = temp;
+		temp = null;
+	    }
+	    // Add the appropriate listener
+	    field.getDocument().addDocumentListener(this);
+	}
+	/** Gives notification that an attribute or set of attributes changed. */
+	public void changedUpdate(DocumentEvent e) {
+	    canEnable();
+	}
+	/** Gives notification that there was an insert into the document. */
+	public void insertUpdate(DocumentEvent e) {
+	    canEnable();
+	}
+        /** Gives notification that a portion of the document has been removed. */
+	public void removeUpdate(DocumentEvent e) {
+	    canEnable();
+	}
+	private void canEnable() {
+	    if(!ignore) {
+		ignore = true;
+		boolean can_enable = true;
+		for(int i = 0; can_enable && i < fields.length; i++) {
+		    can_enable = can_enable && (fields[i].getText().length() > 0);
+		}
+		target.setEnabled(can_enable);
+		ignore = false;
+	    }
+	}
+    }
+
+    private class AttributesListSelectionListener
+	implements ListSelectionListener {
+	private JTable table = null;
+	public AttributesListSelectionListener(JTable table) {
+	    this.table = table;
+	}
+	public void valueChanged(ListSelectionEvent event) {
+	    if(!event.getValueIsAdjusting()) {
+		if((current_attribute = table.getSelectedRow()) != -1) {
+		    edit_attribute.setEnabled(true);
+		    remove_attribute.setEnabled(true);
+		}
+		else {
+		    current_attribute = -1;
+		    edit_attribute.setEnabled(false);
+		    remove_attribute.setEnabled(false);
+		}
+	    }
+	}
+    }
+
+    private class ElementValuesTreeSelectionListener
+	implements TreeSelectionListener {
+	public void valueChanged(TreeSelectionEvent event) {
+	    if(element_values.getSelectionCount() > 0) {
+		// Retrieve the selected value node.
+		TreePath selected_path = element_values.getSelectionPath();
+		current_value_node = (GValueNode) selected_path.getLastPathComponent();
+		edit_value.setEnabled(true);
+		remove_value.setEnabled(true);
+	    }
+	    else {
+		current_value_node = null;
+		edit_value.setEnabled(false);
+		remove_value.setEnabled(false);
+	    }
+	}
+    }
+
+    private class MDSTreeSelectionListener
+	implements TreeSelectionListener {
+	public void valueChanged(TreeSelectionEvent event) {
+	    if(!ignore) {
+		// Clear all variables based on previous tree selection
+		current_collection_file = null;
+		current_element = null;
+		current_set = null;
+		// Now process the node selected
+		TreePath path = event.getPath();
+		current_node = (GEMSNode)path.getLastPathComponent();
+		// What we show depends on the node type...
+		AttributeTableModel atm = null;
+		TreeSet attributes = null;
+		current_attribute_type = current_node.getType();
+		switch(current_attribute_type) {
+		case GEMSNode.COLLECTION:
+		    current_collection_file = (String) current_node.getUserObject();
+		    atm = current_node.getModel();
+		    if(atm == null) {
+			// !!! ArrayList sources = msm.profiler.getSources(current_collection_file);
+			attributes = new TreeSet();
+			// for(int i = 0; i < sources.size(); i++) {
+			// String source = (String) sources.get(i);
+			// !!! String action = msm.profiler.getAction(current_collection_file, source);
+			// if (action == null) {
+			// action = Dictionary.get("MEM.Ignore");
+			// }
+			// attributes.add(new Attribute(source, action));
+			// }
+			atm = new AttributeTableModel(attributes, Dictionary.get("MEM.Source"), Dictionary.get("MEM.Target"), Dictionary.get("MEM.Ignore"));
+			//current_node.setModel(atm);
+		    }
+		    profile_attributes.setModel(atm);
+		    atm.setScrollPane(profile_attributes_scroll);
+		    atm.setTable(profile_attributes);
+
+		    card_layout.show(details_pane, PROFILE);
+		    setControls(true, false, false, true, false, false, true, false, false, false, false, false);
+		    break;
+		case GEMSNode.ELEMENT:
+		    current_element = current_node.getElement();
+		    atm = current_node.getModel();
+		    if(atm == null) {
+			atm = new AttributeTableModel(current_element.getAttributes(), Dictionary.get("MEM.Name"), Dictionary.get("MEM.Language_Code"), Dictionary.get("MEM.Value"), "");
+			//current_node.setModel(atm);
+		    }
+		    element_attributes.setModel(atm);
+		    atm.setScrollPane(element_attributes_scroll);
+		    atm.setTable(element_attributes);
+
+		    // Be careful not to add the keep_root_expanded_listener over and over!
+		    GValueModel value_model = msm.getValueTree(current_element);
+		    if(value_model != null) {
+			value_model.removeTreeModelListener(keep_root_expanded_listener); // Remove it if its already there
+			value_model.addTreeModelListener(keep_root_expanded_listener); // Then add again.
+			element_values.setModel(value_model);
+			element_values_layout.show(element_values_pane, VALUES);
+			// Meanwhile disable/enable controls depending on this Element selection.
+			setControls(true, false, false, false, false, true, true, false, false, true, false, false);
+		    }
+		    else {
+			element_values_layout.show(element_values_pane, BLANK);
+			// Meanwhile disable/enable controls depending on this Element selection.
+			setControls(true, false, false, false, false, false, true, false, false, false, false, false);
+		    }
+		    card_layout.show(details_pane, ELEMENT);
+		    break;
+		case GEMSNode.SET:
+		    current_set = current_node.getSet();
+		    atm = current_node.getModel();
+		    if(atm == null) {
+			NamedNodeMap temp = current_set.getAttributes();
+			attributes = new TreeSet();
+			for(int i = 0; i < temp.getLength(); i++) {
+			    Attr attribute = (Attr) temp.item(i);
+			    // We don't show the namespace attribute, as it is used as a unique primary key and should never be changed or removed in itself.
+			    if(!attribute.getName().equals("namespace")) {
+				attributes.add(new Attribute(attribute.getName(), attribute.getValue()));
+			    }
+			    attribute = null;
+			}
+			atm = new AttributeTableModel(attributes, Dictionary.get("MEM.Name"), Dictionary.get("MEM.Value"), "");
+			//current_node.setModel(atm);
+			temp = null;
+		    }
+		    set_attributes.setModel(atm);
+		    atm.setScrollPane(set_attributes_scroll);
+		    atm.setTable(set_attributes);
+						 
+		    card_layout.show(details_pane, SET);
+		    attributes = null;
+		    // Meanwhile disable/enable controls depending on this Element selection.
+		    setControls(true, true, false, false, true, false, true, false, false, false, false, false);
+		    break;
+		case GEMSNode.PROFILER:
+		    // Meanwhile disable/enable controls depending on this Element selection.
+		    setControls(true, false, true, false, false, false, false, false, false, false, false, false);
+		default:
+		    // Show a blank panel.
+		    card_layout.show(details_pane, BLANK);
+		}
+		attributes = null;
+		path = null;
+		atm = null;
+	    }
+	}
+    }
+
+    private class MEMTreeCellRenderer
+	extends DefaultTreeCellRenderer {
+	public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
+	    super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
+	    setToolTipText(value.toString());
+	    return this;
+	}
+    }
+
+    /** Wrap a ElementWrapper so we get its name when we ask for toString(), not its identifier. */
+    private class NameElementWrapperEntry 
+	implements Comparable {
+	private ElementWrapper element_wrapper = null;
+	public NameElementWrapperEntry(Object object) {
+	    this.element_wrapper = (ElementWrapper) object;
+	}
+	public int compareTo(Object object) {
+	    return element_wrapper.compareTo(object);
+	}
+	public boolean equals(Object object) {
+	    return element_wrapper.equals(object);
+	}
+	public String toString() {
+	    return element_wrapper.getName();
+	}
+    }
+}
Index: /trunk/gli/src/org/greenstone/gatherer/gems/GEMSModel.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/GEMSModel.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/GEMSModel.java	(revision 8270)
@@ -0,0 +1,98 @@
+/**
+ *#########################################################################
+ *
+ * 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.gems;
+
+import javax.swing.tree.DefaultTreeModel;
+import org.greenstone.gatherer.util.SynchronizedTreeModelTools;
+/**
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class GEMSModel 
+    extends DefaultTreeModel {
+
+    public GEMSModel() {
+	super(new GEMSNode());
+    }
+
+    public void add(GEMSNode parent, Object object, int type) {
+	if(parent == null) {
+	    parent = (GEMSNode) root;
+	}
+	GEMSNode new_node = new GEMSNode(type, object, null);
+	SynchronizedTreeModelTools.insertNodeInto(this, parent, new_node);
+    }
+
+    public GEMSNode getProfileNode() {
+	GEMSNode node = (GEMSNode) root;
+	return (GEMSNode) node.getChildAt(node.getChildCount() - 1);
+    }
+
+    public void remove(String name, int type) {
+	GEMSNode current = (GEMSNode) root;
+	for(int i = 0; i < current.getChildCount(); i++) {
+	    GEMSNode child = (GEMSNode) current.getChildAt(i);
+	    if(child.toString().equals(name) && type == GEMSNode.SET) {
+		// Place on AWT event thread to avoid errors.
+		SynchronizedTreeModelTools.removeNodeFromParent(this, child);
+		return;
+	    }
+	    // This may be the profiler root node, in which case we descend into its children if we're looking for collection file matches. 
+	    else if(type == GEMSNode.COLLECTION && child.getType() == GEMSNode.PROFILER) {
+		for(int j = 0; j < child.getChildCount(); j++) {
+		    GEMSNode inner_child = (GEMSNode) child.getChildAt(j);
+		    if(inner_child.toString().equals(name)) {
+			// Place on AWT event thread to avoid errors.
+			SynchronizedTreeModelTools.removeNodeFromParent(this, inner_child);
+			return;
+		    }
+		}
+	    }
+	    // If this is a set and we are looking for an element, then iterate through its children. Can't really do this recursively.
+	    else if(type == GEMSNode.ELEMENT && child.getType() == GEMSNode.SET) {
+		for(int j = 0; j < child.getChildCount(); j++) {
+		    GEMSNode inner_child = (GEMSNode) child.getChildAt(j);
+		    if(inner_child.toString().equals(name)) {
+			// Place on AWT event thread to avoid errors.
+			SynchronizedTreeModelTools.removeNodeFromParent(this, inner_child);
+			return;
+		    }
+		}
+	    }
+	}
+    }
+}
Index: /trunk/gli/src/org/greenstone/gatherer/gems/GEMSNode.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/GEMSNode.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/GEMSNode.java	(revision 8270)
@@ -0,0 +1,228 @@
+/**
+ *#########################################################################
+ *
+ * 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.gems;
+
+import java.io.*;
+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.collection.BasicCollectionConfiguration;
+import org.greenstone.gatherer.util.Utility;
+import org.w3c.dom.*;
+/**
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3
+ */
+public class GEMSNode
+    extends DefaultMutableTreeNode {
+    private AttributeTableModel model = null;
+    private int type = 0;
+    private String text = null;
+    static final public int COLLECTION = 0;
+    static final public int ELEMENT    = 1;
+    static final public int PROFILER   = 2;
+    static final public int ROOT       = 3; 
+    static final public int SET        = 4;
+
+    public GEMSNode() {
+	this.type = ROOT;
+    }
+
+    public GEMSNode(int type, Object object, GEMSNode parent) {
+	this.userObject = object;
+	this.parent = parent;
+	this.type = type;
+	this.text = null;
+    }
+
+    public Enumeration children() {
+	if(children == null) {
+	    mapChildren();
+	}		  
+	return children.elements();
+    }
+
+    public boolean getAllowsChildren() {
+	return true;
+    }
+
+    public TreeNode getChildAt(int index) {
+	if(children == null) {
+	    mapChildren();
+	}
+	return (TreeNode) children.get(index);
+    }
+    public int getChildCount() {
+	if(children == null) {
+	    mapChildren();
+	}
+	return children.size();
+    }
+    public ElementWrapper getElement() {
+	ElementWrapper result = null;
+	if(type == ELEMENT) {
+	    result = (ElementWrapper) userObject;
+	}
+	return result;
+    }
+    public int getIndex(TreeNode node) {
+	if(children == null) {
+	    mapChildren();
+	}
+	return children.indexOf(node);
+    }
+    public AttributeTableModel getModel() {
+	return model;
+    }
+    public TreeNode getParent() {
+	return parent;
+    }
+    public MetadataSet getSet() {
+	MetadataSet result = null;
+	if(type == SET) {
+	    result = (MetadataSet) userObject;
+	}
+	return result;
+    }
+    public int getType() {
+	return type;
+    }
+    public boolean isLeaf() {
+	if(children == null) {
+	    mapChildren();
+	}
+	return children.size() == 0;
+    }
+    public void setModel(AttributeTableModel model) {
+	this.model = model;
+    }
+
+    public String toString() {
+	if(text == null) {
+	    if(userObject != null) {
+		if(userObject instanceof ElementWrapper) {
+		    text = ((ElementWrapper)userObject).getName();
+		}
+		// In the case of a 'collection' source path we need to examine the path closely. If it is a descendant of the greenstone collections path then supress everything but the collection name
+// 		else if(type == COLLECTION) {
+// 		    try {
+// 			boolean is_descendant = false;
+// 			File source_path = new File((String)userObject);
+// 			File collect_path;
+// 			if (Gatherer.GS3) {
+// 			    collect_path = new File(Utility.getCollectDir(Configuration.gsdl3_path, Configuration.site_name));
+// 			}  else {
+// 			    collect_path = new File(Utility.getCollectDir(Configuration.gsdl_path));
+// 			}
+// 			File current_path = source_path;
+// 			while(!is_descendant && current_path != null) {
+// 			    is_descendant = current_path.equals(collect_path);
+// 			    current_path = current_path.getParentFile();
+// 			}
+// 			current_path = null;
+// 			collect_path = null;
+// 			// We've either found we are a descendant, or run out of path
+// 			if(is_descendant) {
+// 			    // Create a basic config class
+// 			    File config_file = new File(source_path, Utility.CONFIG_FILE);
+// 			    if (config_file.exists()) {
+// 				BasicCollectionConfiguration config = new BasicCollectionConfiguration(config_file);
+// 				text = config.toString();
+// 				config = null;
+// 			    } else {
+// 				text = source_path.getAbsolutePath();
+// 			    }
+// 			    config_file = null;
+// 			}
+// 			else {
+// 			    text = source_path.getAbsolutePath();
+// 			}
+// 		    }
+// 		    catch(Exception exception) {
+// 			DebugStream.println("Exception in GEMSNode.toString() - exception is unexpected");
+// 			DebugStream.printStackTrace(exception);
+// 			text = userObject.toString();
+// 		    }
+// 		}
+		else {
+		    text = userObject.toString();
+		}
+	    }
+	    else {
+		text = "error";
+	    }
+	}
+	return text;
+    }
+    private void mapChildren() {
+	///ystem.err.println("Mapping the children of " + this);
+	children = new Vector();
+	// How we build children depends on the node type
+	switch(type) {
+	case PROFILER: // Add the collections as children
+	    // ArrayList a = GEMS.msm.profiler.getCollections();
+	    // for(int i = 0; i < a.size(); i++) {
+	    // children.add(new GEMSNode(COLLECTION, a.get(i), this));
+	    // }
+	    // a = null;
+	    break;
+	case ROOT:
+	    Vector v = GEMS.msm.getSets();
+	    for(int i = 0; i < v.size(); i++) {
+		children.add(new GEMSNode(SET, v.get(i), this));
+	    }
+	    v = null;
+	    // Add the profile set.
+	    children.add(new GEMSNode(PROFILER, Dictionary.get("MEM.Profiles"), this));
+	    break;
+	case SET: // Add the elements as children
+	    MetadataSet set = (MetadataSet) userObject;
+	    NodeList elements = set.getElements();
+	    set = null;
+	    for(int i = 0; i < elements.getLength(); i++) {
+		children.add(new GEMSNode(ELEMENT, new ElementWrapper((Element) elements.item(i)), this));
+	    }
+	    elements = null;
+	    break;
+	case COLLECTION:
+	case ELEMENT:
+	default:
+	}
+    }
+}
Index: /trunk/gli/src/org/greenstone/gatherer/gems/GValueModel.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/GValueModel.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/GValueModel.java	(revision 8270)
@@ -0,0 +1,284 @@
+/**
+ *#########################################################################
+ *
+ * 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.gems;
+
+import java.util.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.util.PatternTokenizer;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.w3c.dom.*;
+/*
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.1
+ */
+public class GValueModel
+    extends DefaultTreeModel {
+
+    private ElementWrapper element;
+
+    public GValueModel() {
+	super(new DefaultMutableTreeNode("Temp"));
+    }
+
+    public GValueModel(String root) {
+	super(new DefaultMutableTreeNode(root));
+    }
+
+    public GValueModel(ElementWrapper e) {
+	super(new DefaultMutableTreeNode("Temp"));
+	this.element = e;
+	// Load the template value tree document.
+	Document document = MSMUtils.getValueTreeTemplate();
+	Element new_root = document.getDocumentElement();
+	new_root.setAttribute("element", e.getName());
+	root = new GValueNode(new_root);
+    }
+
+    public GValueModel(ElementWrapper e, Document document) {
+	super(new DefaultMutableTreeNode("Temp"));
+	this.element = e;
+	Element new_root = document.getDocumentElement();
+	new_root.setAttribute("element", e.getName());
+	root = new GValueNode(new_root);
+    }
+
+    /** Value may include path ie news\newssw */
+    public GValueNode addValue(String value) {
+       try {
+	    // Tokenize the string using the escaped character
+	    PatternTokenizer tokenizer = new PatternTokenizer(value, StaticStrings.PIPE_STR);
+	    GValueNode subject = (GValueNode) root;
+	    while(tokenizer.hasMoreTokens() && subject != null) {
+		String token = tokenizer.nextToken();
+		subject = addValue(token, subject);
+	    }
+	    return subject;
+	}
+	catch (Exception error) {
+	    error.printStackTrace();
+	}
+	return null;
+    }
+
+   public GValueNode addValue(String value, GValueNode subject) {
+	// To add a value we must first ensure it isn't already present in -this- nodes children. The bummer is that the nice getElements functions search the whole tree so...
+	GValueNode value_node = subject.getValue(value);
+	if(value_node == null) {
+	    // Now add the new value.
+	    Document document = subject.getElement().getOwnerDocument();
+	    // Now we create a new subject and add it subject
+	    Element new_subject = document.createElementNS("","Subject");
+	    Element new_value = document.createElementNS("","Value");
+	    new_subject.appendChild(new_value);
+	    Text new_text = document.createTextNode(value);
+	    new_value.appendChild(new_text);
+	    value_node = new GValueNode(new_subject);
+	    ///ystem.err.println("(GValueModel) addValue()... " + value_node);
+
+	    // Figure out where this node will be inserted in subjects
+	    // children.
+	    int position = -1;
+	    for(int i = 0; position == -1 && i < subject.getChildCount(); i++) {
+		Object sibling = subject.getChildAt(i);
+		int rel_pos = value.compareTo(sibling.toString());
+		///ystem.err.println("'"+value+"'.compareTo('"+sibling+"') = " + rel_pos);
+		if(rel_pos <= 0) {
+		    position = i;
+		}
+	    }
+	    if(position == -1) {
+		position = subject.getChildCount();
+	    }
+	    // Insert it. If position is still -1, append it to the end of subjects children.
+	    ///ystem.err.println("Inserting '" + value + "' at position " + position);
+	    insertNodeInto(value_node, subject, position);
+	    ///ystem.err.println("(GValueModel) Done insert node into...");
+	    // SynchronizedTreeModelTools.insertNodeInto(this, subject, value_node);
+	    // Inform listeners that we've changed.
+	    // Gatherer.c_man.getCollection().msm.fireValueChanged(element, null, this);
+	}
+	return value_node;
+    }
+
+    public Document getDocument() {
+	return ((GValueNode)root).getElement().getOwnerDocument();
+    }
+
+    public ElementWrapper getElement() {
+	return element;
+    }
+
+    /** Retrieve the hindex for a certain value within the value tree.
+     * @param value The value whose index you wish to determine as a <strong>String</strong>.
+     * @return A <strong>String</strong> containing an index such as "1", "2.1" or "18.2.5".
+     */
+    public String getHIndex(String value) {
+	///ystem.err.println("getHIndex(" + value + ")");
+	return getHIndex((GValueNode) root, value, null);
+    }
+
+    /** Retrieve a value node given its hierarchical reference or value.
+     * @param index_str The hierarchy index or value as a <strong>String</strong>.
+     */
+    public GValueNode getValue(String index_str) {
+	///ystem.err.println("Retrieve the value for: " + index_str);
+	GValueNode result = null;
+	if(isHierarchy() && Utility.isIndex(index_str)) {
+	    // StringTokenize the index
+	    StringTokenizer tokenizer = new StringTokenizer(index_str, ".");
+	    result = (GValueNode) root;
+	    // Using the index numbers retrieve the appropriate node.
+	    try {
+		while(result != null && tokenizer.hasMoreTokens()) {
+		    int index = Integer.parseInt(tokenizer.nextToken()) - 1;
+		    // Retrieve the index'th child of the current result node
+		    if(0 <= index && index < result.getChildCount()) {
+			result = (GValueNode) result.getChildAt(index);
+		    }
+		    // Otherwise we're broken.
+		    else {
+			///ystem.err.println("There is no " + index + "th childnode of " + result);
+			result = null;
+		    }
+		}
+	    }
+	    // Most likely caused by parseInt throwing a wobbly.
+	    catch (Exception error) {
+		result = null;
+	    }
+	}
+	if(result == null) {
+	    ///ystem.err.println("No existing value. Adding " + index_str);
+	    result = addValue(index_str);
+	}
+	return result;
+    }
+
+    public boolean isHierarchy() {
+	boolean result = false;
+	// We are a hierarchy if our element says so....
+	if(element.isHierarchy()) {
+	    return true;
+	}
+	// Or if our children are actually a hierarchy.
+	for(int i = 0; i < root.getChildCount() && !result; i++) {
+	    GValueNode node = (GValueNode) root.getChildAt(i);
+	    if(node != null && node.getChildCount() > 0) {
+		result = true;
+	    }
+	}
+	return result;
+    }
+
+    public void removeValue(GValueNode child) {
+	//SynchronizedTreeModelTools.removeNodeFromParent(this, child);
+	removeNodeFromParent(child);
+	// Gatherer.c_man.getCollection().msm.fireValueChanged(new ElementWrapper(child.getElement()), null, this);
+    }
+
+    public void removeValue(String value) {
+	// Retrieve the node to be removed.
+	GValueNode node = getValue(value);
+	if(node != null) {
+	    removeValue(node);
+	}
+    }
+
+    public int size() {
+	return size(root);
+    }
+
+    private int size(TreeNode current) {
+	int size = 1;
+	for(int i = 0; i < current.getChildCount(); i++) {
+	    size = size + size(current.getChildAt(i));
+	}
+	return size;
+    }
+
+    public String toString() {
+	return element.toString();
+    }
+
+    public Vector traverseTree() {
+	Vector contents = new Vector();
+	contents.addAll(traverseTree((GValueNode) root));
+	contents.remove((GValueNode) root);
+	return contents;
+    }
+
+    private GValueModel copy() {
+	Element document_element = ((GValueNode) root).getElement();
+	Document document_copy = MSMUtils.getValueTreeTemplate();
+	Element document_copy_element = document_copy.getDocumentElement();
+	document_copy_element.setAttribute("element", element.getName());
+	for(Node node = document_element.getFirstChild(); node != null; node = node.getNextSibling()) {
+	    if(node.getNodeName().equals("Subject")) {
+		Node node_copy = document_copy.importNode(node, true);
+		document_copy_element.appendChild(node_copy);
+	    }
+	}
+	return new GValueModel(element, document_copy);
+    }
+
+    private String getHIndex(GValueNode node, String value, String index) {
+	///ystem.err.println("Get the HIndex for: " + value);
+	for(int i = node.size(); i != 0; i--) {
+	    GValueNode next = (GValueNode)node.get(i - 1);
+	    String next_str = next.toString(GValueNode.DOM);
+	    ///ystem.err.println("Does " + value + " start with " + next_str + "?");
+	    if(value.startsWith(next_str)) {
+		if(index == null) {
+		    index = String.valueOf(i);
+		}
+		else {
+		    index = index + "." + i;
+		}
+		value = value.substring(next.toString().length());
+		///ystem.err.println("We matched the start so value is now: " + value);
+		if(value.startsWith(StaticStrings.PIPE_STR) || value.startsWith(StaticStrings.ESCAPE_STR)) {
+ 		    value = value.substring(1);
+		    index = getHIndex(next, value, index);
+		}
+		///ystem.err.println("Returning " + index);
+		return index;
+	    }
+	}
+	///ystem.err.println("Returning " + index);
+	return index;
+    }
+
+    private Vector traverseTree(GValueNode node) {
+	Vector contents = new Vector();
+	contents.add(node);
+	for(int i = 0; i < node.getChildCount(); i++) {
+	    contents.addAll(traverseTree((GValueNode)node.getChildAt(i)));
+	}
+	return contents;
+    }
+}
Index: /trunk/gli/src/org/greenstone/gatherer/gems/GValueNode.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/GValueNode.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/GValueNode.java	(revision 8270)
@@ -0,0 +1,407 @@
+/**
+ *#########################################################################
+ *
+ * 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.gems;
+
+/**
+ * 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>
+ * First Coded:  20/06/02
+ * Revised:<br>
+ * @author John Thompson, Greenstone Digital Libraries
+ * @version 2.1
+ */
+
+import java.util.*;
+import javax.swing.tree.*;
+import org.greenstone.gatherer.util.Codec;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.XMLTools;
+import org.w3c.dom.*;
+
+/** This class is instantiated using a metadata element. From this parent element, which refers to a specific Element in the mds DOM model, the AssignedValues child is used as the root of a tree data-structure encompassing all of the nodes within the AssignedValues tree. This class also provide methods for adding, updating and removing nodes within this tree, as well as implementing all of the methods necessary for this node to be used as the basis of a tree model.
+ */
+public class GValueNode
+    extends DefaultMutableTreeNode {
+
+   static final public int DOM = 0;
+   static final public int GREENSTONE = 1;
+   static final public int TEXT = 2;
+
+    private String element_name = null;
+    private String default_value = null;
+
+    public GValueNode(Element element) {
+	this.children = null;
+	this.userObject = element;
+	this.default_value = null;
+    }
+
+    public GValueNode(String element_name, String default_value) {
+	this.element_name = element_name;
+	this.default_value = default_value;
+    }
+
+    /** Compares two GValueNodes for ordering by using the String.compareTo method.
+     * @param sibling The <strong>Object</strong> representing the GValueNode we are comparing ourselves to.
+     * @return An <i>int</i> indicating the ordering. The value is less than zero if we are before the parameter object, zero if we are equal and greater than zero if we preceed sibling.
+     */
+    public int compareTo(Object sibling) {
+	return toString().compareTo(sibling.toString());
+    }
+
+    /** Returns an enumeration of the child nodes.
+     * @return An <strong>Enumeration</strong> containing the child nodes.
+     */
+    public Enumeration children() {
+	if(default_value != null) {
+	    return null;
+	}
+	if(children == null) {
+	    map();
+	}
+	return children.elements();
+    }
+    public boolean equals(Object other) {
+	return compareTo(other) == 0;
+    }
+
+    public GValueNode get(int index) {
+	GValueNode result = null;
+	if(children != null) {
+	    result = (GValueNode) children.get(index);
+	}
+	return result;
+    }
+
+    /** Returns <I>true</I> if the reciever allows children. */
+    public boolean getAllowsChildren() {
+	return true;
+    }
+
+    /** Returns the child <I>TreeNode</I> at index <I>childIndex</I>.*/
+    public TreeNode getChildAt(int index) {
+	if(default_value != null) {
+	    return this;
+	}
+	if(children == null) {
+	    map();
+	}
+	return (GValueNode) children.get(index);
+    }
+
+    /** Returns the number of children <I>TreeNode</I>s the reciever contains. */
+    public int getChildCount() {
+	int size = 0;
+	if(default_value != null) {
+	    size = 0;
+	}
+	if(children == null) {
+	    map();
+	}
+	return children.size();
+    }
+
+    public Element getElement() {
+	return (Element) userObject;
+    }
+
+    /** Return the path to this node within the value tree model.
+     * @return A <strong>String</strong> representing the path, ie "Titles\Modern\The Green Mile"
+     */
+    public String getFullPath(boolean as_text) {
+	if(default_value != null) {
+	    return default_value;
+	}
+	StringBuffer path = new StringBuffer(toString(DOM));
+	GValueNode node = (GValueNode) getParent();
+	while(node != null && !node.getElement().getNodeName().equalsIgnoreCase("AssignedValues")) {
+	    path.insert(0, StaticStrings.PIPE_STR);
+	    path.insert(0, node.toString(DOM));
+	    node = (GValueNode) node.getParent();
+	}
+	if(as_text) {
+	    String temp = path.toString(); ///odec.transform(path.toString(), codec.GREENSTONE_TO_TEXT);
+	    ///ystem.err.println("Raw: " + temp);
+	    temp = Codec.transform(temp, Codec.DECODE_PATH);
+	    temp = Codec.transform(temp, Codec.DECODE_SQUARE_BRACKETS);
+	    ///ystem.err.println("Decoded: " + temp);
+	    return temp;
+	}
+	else {
+	    return path.toString();
+	}
+    }
+
+    /** Returns the index of <I>node</I> in the recievers children. If the
+     * reciever does not contain <I>node</I>, <I>-1</I> will be returned. */
+    public int getIndex(TreeNode node) {
+	if(default_value != null) {
+	    return 0;
+	}
+	if(children == null) {
+	    map();
+	}
+	return children.indexOf(node);
+    }
+
+    public String getMetadataElementName() {
+	if(default_value != null) {
+	    return element_name;
+	}
+	GValueNode node = this;
+	while(node != null && !node.getElement().getNodeName().equalsIgnoreCase("AssignedValues")) {
+	    node = (GValueNode)node.getParent();
+	}
+	if(node != null) {
+	    return node.getElement().getAttribute("element");
+	}
+	return null;
+    }
+
+
+    public GValueNode getValue(String value)
+    {
+	///ystem.err.println("GValueNode.getValue(" + value + ")");
+	if (default_value != null) {
+	    return this;
+	}
+
+	if (children == null) {
+	    map();
+	}
+
+	for (int i = 0; i < children.size(); i++) {
+	    GValueNode child = (GValueNode) children.get(i);
+	    // System.err.println("Comparing " + value + " and " + child.toString(GValueNode.DOM));
+	    if (value.equals(child.toString(GValueNode.DOM))) {
+		return child;
+	    }
+	}
+
+	return null;
+    }
+
+
+    /** Adds <I>child</I> to the receiever at <I>index</I>. <I>child</I> will
+     * be messaged with setParent().
+     */
+    public void insert(MutableTreeNode child, int index) {
+	if(default_value != null) {
+	    return;
+	}
+	if(index >= children.size()) {
+	    // Append to children.
+	    children.add(child);
+	    // And to document
+	    getElement().appendChild(((GValueNode)child).getElement());
+	}
+	else {
+	    GValueNode sibling = (GValueNode) children.get(index);
+	    // Insert in children
+	    children.add(index, child);
+	    // And in document
+	    getElement().insertBefore(((GValueNode)child).getElement(), sibling.getElement());
+	}
+	child.setParent(this);
+    }
+
+    /** Returns <I>true</I> if the reciever is a leaf. */
+    public boolean isLeaf() {
+	if(default_value != null || getChildCount() > 0) {
+	    return false;
+	}
+	return true;
+    }
+
+    /** Ensure that the children nodes of this tree are instantiated, and record the initial mappings for legacy sake.
+     * @param mapping A <strong>HashMap</strong> into which to write the initial mappings.
+     * @param prefix The prefix to use for indexes, as a <strong>String</strong>.
+     */
+    public void map(HashMap mapping, String prefix) {
+	if(default_value != null) {
+	    return;
+	}
+	children = new Vector();
+	if(prefix.length() > 0) {
+	    prefix = prefix + ".";
+	}
+	int i = 1;
+	for(Node node = getElement().getFirstChild(); node != null; node = node.getNextSibling()) {
+	    if(node.getNodeName().equals("Subject")) {
+		GValueNode child = new GValueNode((Element)node);
+		child.setParent(this);
+		children.add(child);
+		String index = prefix + i;
+		mapping.put(index, child);
+		child.map(mapping, index);
+		i++;
+	    }
+	}
+    }
+
+    /** Resets the user object of the reciever to <I>object</I>. */
+    public void setUserObject(Object object) {
+	// Can't ever change Elements from here. Have to use editor. */
+    }
+
+    public void setValue(String value) {
+	setNodeValue("Value", value);
+    }
+
+    public int size() {
+	int size = 0;
+	if(children != null) {
+	    size = children.size();
+	}
+	return size;
+    }
+
+    /** Returns a <I>String</I> representation of the reciever. If this
+     * happens to be the AssignedValues 'root' then we do something slightly
+     * different, otherwise we return the value of the <I>#Text</I> child of
+     * the <I>Value</I> of this <I>Subject</I> node!
+     */
+    public String toString() {
+	return toString(GValueNode.TEXT);
+    }
+
+    public String toString(int decode_type) {
+	if(default_value != null) {
+	    return default_value;
+	}
+	Element element = getElement();
+	String name = element.getNodeName();
+	String result = null;
+	if(name.equals("Subject")) {
+	    result = XMLTools.getValue(element);
+	    ///ystem.err.print(result);
+	    switch(decode_type) {
+	    case GValueNode.GREENSTONE:
+		// We want this as greenstone format
+		///ystem.err.print(result);
+		//result = /odec.transform(result, /odec.DOM_TO_GREENSTONE);
+		///ystem.err.println(" -> D2G decode -> " + result);
+		break;
+	    case GValueNode.TEXT:
+		//result = /odec.transform(result, /odec.DOM_TO_TEXT);
+		result = Codec.transform(result, Codec.DECODE_PATH);
+		result = Codec.transform(result, Codec.DECODE_SQUARE_BRACKETS);
+		///ystem.err.println(" -> D2T decode -> " + result);
+		break;
+	    default:
+		///ystem.err.println(result + " -> nothing to do.");
+	    }
+	}
+	else if(name.equals("AssignedValues")) {
+	    result = getMetadataElementName();
+	}
+	return result;
+    }
+
+    private void map() {
+	children = new Vector();
+	for(Node node = getElement().getFirstChild(); node != null; node = node.getNextSibling()) {
+	    if(node.getNodeName().equals("Subject")) {
+		GValueNode child = new GValueNode((Element)node);
+		child.setParent(this);
+		children.add(child);
+	    }
+	}
+    }
+
+    /** Removes the child at index from the receiver. */
+    public void remove(int index) {
+	remove((MutableTreeNode)getChildAt(index));
+    }
+
+    /** Removes node from the receiver. */
+    public void remove(MutableTreeNode node) {
+	children.remove(node);
+	node.setParent(null);
+	// Remove from DOM
+	Element child_element = ((GValueNode)node).getElement();
+	Node parent_node = child_element.getParentNode();
+	parent_node.removeChild(child_element);
+    }
+
+    /** Removes the receiver from its parent. */
+    public void removeFromParent() {
+	if(parent != null) {
+	    parent.remove(this);
+	}
+    }
+
+    private void setNodeValue(String name, String value) {
+	boolean found = false;
+	// Attempt to retrieve a child node named name
+	Element element = (Element) userObject;
+	for(Node pos_node = element.getFirstChild(); pos_node != null; pos_node = pos_node.getNextSibling()) {
+	    // And if we find such a node
+	    if(pos_node.getNodeName().equals(name)) {
+		// And the new value is non-null, retrieve its text node and change it.
+		if(value != null) {
+		    for(Node pos_text = pos_node.getFirstChild(); pos_text != null; pos_text = pos_text.getNextSibling()) {
+			// When we find the node...
+			if(pos_text.getNodeName().equals("#text")) {
+			    pos_text.setNodeValue(value);
+			}
+		    }
+		}
+		// Otherwise remove the node altogether
+		else {
+		    element.removeChild(pos_node);
+		}
+		found = true;
+	    }
+	}
+	// Otherwise if no such node exists add it.
+	if(!found && value != null) {
+	    Document document = element.getOwnerDocument();
+	    Node new_node = document.createElementNS("", name);
+	    element.appendChild(new_node);
+	    Node new_text = document.createTextNode(value);
+	    new_node.appendChild(new_text);
+	    new_text = null;
+	    new_node = null;
+	}
+	// Done.
+	element = null;
+	return;
+    }
+}
Index: /trunk/gli/src/org/greenstone/gatherer/gems/MSMUtils.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/MSMUtils.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/MSMUtils.java	(revision 8270)
@@ -0,0 +1,1045 @@
+/**
+ *#########################################################################
+ *
+ * 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.gems;
+
+/**************************************************************************************
+ * 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 
+ *               06/08/03 Bug fixes
+ * @author John Thompson, Greenstone Digital Libraries
+ * @version 2.3
+ **************************************************************************************/
+import java.io.*;
+import java.util.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.cdm.CommandTokenizer;
+import org.greenstone.gatherer.util.ArrayTools;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.w3c.dom.*;
+/** This class contains a plethora of methods associated with handling the content of <strong>MetadataSet</strong>s and the <strong>Element</strong>s within. For example this is where you will find methods for comparing various parts of two <strong>MetadataSet</strong>s for equality. It also has methods for extracting common and useful data from <strong>Element</strong>s such as the AssignedValue nodes.
+ * @author John Thompson, Greenstone Digital Libraries
+ * @version 2.3
+ */
+public class MSMUtils {
+    /** Used to order metadata according to set standard element order then alphabetically by value. */
+    // static private MetadataComparator METADATA_COMPARATOR = new MetadataComparator();
+    /** An element of the enumeration of type filter. */
+    static public int NONE = 0;
+    /** An element of the enumeration of type filter. */
+    static public int VALUES = 1;
+    /** An element of the enumeration of type filter. */
+    static public int ALIASES = 2;
+    /** An element of the enumeration of type filter. */
+    static public int BOTH = 3;
+    /** The character used to separate name space from metadata element. */
+    static public char NS_SEP= '.';
+    /** The character used to separate subfields from metadata element. */
+    static public String SF_SEP= "#";
+    /** Method to add one node as a child of another, after migrating into the target document.
+     * @param parent The <strong>Node</strong> we are inserting into.
+     * @param child The original <strong>Node</strong> we are inserting. Must first be cloned into the parents document.
+     */
+    static public void add(Node parent, Node child) {
+	Document document = parent.getOwnerDocument();
+	Node new_child = document.importNode(child, true);
+	parent.appendChild(new_child);
+    } 
+
+    /** Method to add an attribute element to the given element. This method makes use of the language_dependant attribute of the document to not only determine if the attribute is language dependant, but also to see whether a Language element should be created if doesn't already exist.
+     * @param element_element the Element to add the attribute element to
+     * @param attribute_name_str the name of the new attribute to add as a String
+     * @param language_code_str the two letter code String of the language this attribute is to be added as
+     * @param value_str the String to be assigned as the attribute elements value
+     * @see org.greenstone.gatherer.msm.MSMUtils#isAttributeLanguageDependant
+     * @see org.greenstone.gatherer.msm.MSMUtils#setValue(Element, String)
+     * @see org.greenstone.gatherer.util.StaticStrings#ATTRIBUTE_ELEMENT
+     * @see org.greenstone.gatherer.util.StaticStrings#CODE_ATTRIBUTE
+     * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGE_ATTRIBUTE
+     * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGE_ELEMENT
+     * @see org.greenstone.gatherer.util.StaticStrings#NAME_ATTRIBUTE
+     */ 
+    static public void addElementAttribute(Element element_element, String attribute_name_str, String language_code_str, String value_str) {
+	Document document = element_element.getOwnerDocument();
+	// Create the basic new attribute (everything except language attribute)
+	Element attribute_element = document.createElement(StaticStrings.ATTRIBUTE_ELEMENT);
+	attribute_element.setAttribute(StaticStrings.NAME_ATTRIBUTE, attribute_name_str);
+	MSMUtils.setValue(attribute_element, value_str);
+	// Start off by determining if we have to add this node in the new multilingual optimized way
+	if(isAttributeLanguageDependant(document, attribute_name_str)) {
+	    boolean found = false;
+	    // Try to retrieve a language element for the given language code
+	    NodeList language_elements = element_element.getElementsByTagName(StaticStrings.LANGUAGE_ELEMENT);
+	    for(int i = 0; i < language_elements.getLength(); i++) {
+		Element language_element = (Element) language_elements.item(i);
+		if(language_element.getAttribute(StaticStrings.CODE_ATTRIBUTE).equals(language_code_str)) {
+		    found = true;
+		    // Add attribute
+		    language_element.appendChild(attribute_element);
+		}
+		language_element = null;
+	    }
+	    language_elements = null;
+	    // If it still hasn't been found, then add it
+	    if(!found) {
+		Element language_element = document.createElement(StaticStrings.LANGUAGE_ELEMENT);
+		language_element.setAttribute(StaticStrings.CODE_ATTRIBUTE, language_code_str);
+		element_element.appendChild(language_element);
+		// Add attribute
+		language_element.appendChild(attribute_element);
+		language_element = null;
+	    }
+	}
+	// Just add the attribute the old fashioned way
+	else {
+	    attribute_element.setAttribute(StaticStrings.LANGUAGE_ATTRIBUTE, language_code_str);
+	    element_element.appendChild(attribute_element);
+	}
+	// Clean up
+	attribute_element = null;
+	document = null;
+    }
+
+    /** A method for comparing two AssignedValues trees. This compares not only the Subject hierarchies but also the values themselves.
+     * @param avt A <strong>Node</strong> which is the root of an AssignedValues tree.
+     * @param bvt The <strong>Node</strong> which is the root of the tree we wish to compare it to.
+     * @return <i>true</i> if the two trees are equal, <i>false</i> otherwise.
+     */
+    static final private boolean assignedValuesEqual(Node avt, Node bvt) {
+	if(avt == null && bvt == null) {
+	    return true; // Both are null so both are equal.
+	}
+	else if(avt == null || bvt == null) {
+				// One is null and the other isn't.
+	    return false;
+	}
+	else {
+	    Hashtable a_map = new Hashtable();
+	    getValueMappings(avt, null, a_map);
+	    Hashtable b_map = new Hashtable();
+	    getValueMappings(bvt, null, b_map);
+	    if(a_map.size() == b_map.size()) {
+		/** @TODO - Figure out what to do now. */
+		return true;
+	    }
+	}
+	return false;
+    }
+    /** A method for comparing two attribute nodes of type Node not Attr as you might think. Attr objects are used to describe the attributes of tags themselves, while in a metadata set we intend attribute nodes to describe qualities of metadata elements. It's just confusing because the two systems (DOM model and Dublin Core) are quite similar.
+     * @param an A <strong>Node</strong> representing some attribute of an element.
+     * @param bn The <strong>Node</strong> we wish to compare it to.
+     * @return <i>true</i> if and only if the attributes are equal.
+     */
+    static final public boolean attributesEqual(Node an, Node bn) {
+	// Check we are comparing apples and apples...
+	if(an.getNodeName().equals("Attribute") && bn.getNodeName().equals("Attribute")) {
+	    Element ae = (Element) an;
+	    Element be = (Element) bn;
+				// Ensure we are comparing the same type of attribute.
+	    if(ae.getAttribute("name").equals(be.getAttribute("name"))) {
+		// And finally retrieve and compare the values.
+		if(getValue(ae).equals(getValue(be))) {
+		    // We have a match.
+		    return true;
+		}
+	    }
+	}
+	// And if anything goes wrong we can't be dealing with equal attributes.
+	return false;
+    }
+
+    /** Remove all of the child nodes from a certain node. */
+    static final public void clear(Node parent) {
+	while(parent.hasChildNodes()) {
+	    parent.removeChild(parent.getFirstChild());
+	}
+    }
+
+    /** Method to compare two metadata elements (of type Element, which is bound to get more than a bit confusing) for equality. This test may only check the structural (ie pretty much unchanging) consistancy, or may include the AssignedValue tree as well (which will be different for each collection I'd imagine).
+     * @param a_set The <strong>MetadataSet</strong> a comes from.
+     * @param ae An <strong>Element</strong>.
+     * @param b_set The <strong>MetadataSet</strong> b comes from.
+     * @param be The <strong>Element</strong> to compare it to.
+     * @param values <i>true</i> if the AssignedValues tree should also be compared, <i>false</i> otherwise.
+     * @return <i>true</i> if the elements are equal, <i>false</i> otherwise.
+     */
+    static final public boolean elementsEqual(MetadataSet a_set, Element ae, MetadataSet b_set, Element be, boolean values) {
+	// Compare Element Attr(ibutes) within the DOM, not to be confused with comparing element attributes in a Dublin Core sense...
+	NamedNodeMap aas = ae.getAttributes();
+	NamedNodeMap bas = be.getAttributes();
+	// For each attribute in a...
+	for(int i = 0; i < aas.getLength(); i++) {
+	    Attr aa = (Attr)aas.item(i);
+				// Try to retrieve an attribute of the same name from b.
+	    Attr ba = (Attr)bas.getNamedItem(aa.getNodeName());
+				// Now if there was no such attribute, or if the values for the
+ 				// two attributes are different the structures different.
+	    if(ba == null || (!aa.getValue().equals(ba.getValue()))) {
+		//ystem.err.println("Attributes are not equal");
+		return false;
+	    }
+	}
+	// Quickest test of children is to see we have the same number in
+	// each. Remember to modify for missing AssignedValues which have
+	// nothing to do with structure.
+	int anc = getAttributeCount(ae);
+	int bnc = getAttributeCount(be);
+	if(anc != bnc) {
+	    return false;
+	}
+	// Now we compare the child nodes of the two Elements taking into 
+	// account three special cases...
+	// 1. We don't test the AssignedValues element here.
+	// 2. Remember OptionList node.
+	// 3. The Attributes of each metadata element.
+	// For each child node of a.
+	for(Node an = ae.getFirstChild(); an !=null; an =an.getNextSibling()) {
+	    if(an.getNodeName().equals("OptionList")) {
+		//ystem.err.println("Matching OptionLists.");
+		Node bn = getNodeFromNamed(be, "OptionList");
+		if(bn == null || !optionListsEqual(an, bn)) {
+		    //ystem.err.println("OptionLists are not equal");
+		    return false;
+		}
+	    }
+				// Matching attributes.
+	    else if(an.getNodeName().equals("Attribute")) {
+		//ystem.err.println("Matching Attributes.");
+		boolean matched = false;
+		for(Node bn = be.getFirstChild(); bn != null && !matched;
+		    bn = bn.getNextSibling()) {
+		    if(bn.getNodeName().equals("Attribute")) {
+			matched = attributesEqual(an, bn);
+		    }
+		}
+		if(!matched) {
+		    //ystem.err.println("Cannot match attribute.");
+		    return false;
+		}
+	    }
+	}
+	// Finally, if we've been asked to compares value trees (for some unknown reason) go ahead and compare them too.
+	if(values) {
+	    GValueModel avt = a_set.getValueTree(new ElementWrapper(ae));
+	    GValueModel bvt = b_set.getValueTree(new ElementWrapper(be));
+	    return assignedValuesEqual(avt.getDocument().getDocumentElement(), bvt.getDocument().getDocumentElement());
+	}
+	// If we've got this far the elements match!
+	return true;
+    }	
+    /** This method extracts the assigned value trees details, if any, from a certain element and stores them in an array ready to be passed as arguments to the Dictionary.
+     * @param element The <strong>Element</strong> whose values we wish to view.
+     * @return A <strong>String[]</strong> containing the details of the assigned values tree.
+     * @see org.greenstone.gatherer.Dictionary
+     */
+    static final public String[] getAssignedValuesDetails(MetadataSet mds, Element element) {
+	String details[] = null;
+	//Node avt = getNodeFromNamed(element, "AssignedValues");
+	GValueModel avt = mds.getValueTree(new ElementWrapper(element));
+	if(avt != null) {
+	    Hashtable mapping = new Hashtable();
+	    getValueMappings(avt.getDocument().getDocumentElement(), null, mapping);
+	    ArrayList values = new ArrayList(mapping.keySet());
+	    Collections.sort(values);
+	    details = new String[1];
+	    for(int i = 0; i < values.size(); i++) {
+		if(details[0] == null) {
+		    details[0] = "   " + values.get(i);
+		}
+		else {
+		    details[0] = details[0] + "\n   " + values.get(i);
+		}
+	    }
+	    mapping = null;
+	    values = null;
+	}
+	avt = null;
+	return details;
+    }
+
+    /** Retrieve all of the attributes for the given element as a tree set. Note that this requires significant manipulation if the source is a multilingual optimized metadata set.
+     * @param element the Element whose attributes we wish to catalog
+     * @return a TreeSet of the attributes sorted by their natural ordering
+     * @see org.greenstone.gatherer.msm.MSMUtils#getValue(Node)
+     * @see org.greenstone.gatherer.util.StaticStrings#ATTRIBUTE_ELEMENT
+     * @see org.greenstone.gatherer.util.StaticStrings#CODE_ATTRIBUTE
+     * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGE_ATTRIBUTE
+     * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGE_ELEMENT
+     * @see org.greenstone.gatherer.util.StaticStrings#NAME_ATTRIBUTE
+     */
+    static public TreeSet getAttributes(Element element) {
+	TreeSet attribute_tree = new TreeSet();
+	for(Node node = element.getFirstChild(); node != null; node = node.getNextSibling()) {
+	    if(node instanceof Element) {
+		Element some_element = (Element) node;
+		String some_element_name = some_element.getNodeName();
+		if(some_element_name.equals(StaticStrings.ATTRIBUTE_ELEMENT)) {
+		    attribute_tree.add(new Attribute(some_element.getAttribute(StaticStrings.NAME_ATTRIBUTE), some_element.getAttribute(StaticStrings.LANGUAGE_ATTRIBUTE), MSMUtils.getValue(some_element)));
+		}
+		else if(some_element_name.equals(StaticStrings.LANGUAGE_ELEMENT)) {
+		    String language_code = some_element.getAttribute(StaticStrings.CODE_ATTRIBUTE);
+		    NodeList attribute_elements = some_element.getElementsByTagName(StaticStrings.ATTRIBUTE_ELEMENT);
+		    for(int i = 0; i < attribute_elements.getLength(); i++) {
+			Element attribute_element = (Element) attribute_elements.item(i);
+			attribute_tree.add(new Attribute(attribute_element.getAttribute(StaticStrings.NAME_ATTRIBUTE), language_code, MSMUtils.getValue(attribute_element)));
+			attribute_element = null;
+		    }
+		    attribute_elements = null;
+		    language_code = null;
+		}
+		some_element_name = null;
+		some_element = null;
+	    }
+	}
+	return attribute_tree;
+    }
+
+    /** Method to count the number of Attribute nodes under a certain Element. This ignores other nodes such as #text, OptionList and AssignedValues nodes.
+     * @param element The <strong>Element</strong> whose attributes you want to count.
+     * @return An <i>int</i> which is the number of attribute nodes.
+     */
+    static final private int getAttributeCount(Node element) {
+	int count = 0;
+	for(Node n = element.getFirstChild(); n != null; 
+	    n = n.getNextSibling()) {
+	    if(n.getNodeName().equals("Attribute")) {
+		count++;
+	    }
+	}
+	return count;
+    }
+
+    /** This method is a slight variation on getNodeNamed in that it is especially written to retrieve the attribute Nodes of a certain name present under the given element.
+     * @param element The target element <strong>Node</strong>.
+     * @param name The name of the attribute you wish to return.
+     * @return An <strong>Element[]</strong> containing the attributes you requested, or <i>null</i> if no such attributes exists.
+     */
+    static final public Element[] getAttributeNodesNamed(Node element, String name) { 
+	Element attributes[] = null;
+	for(Node n = element.getFirstChild(); n != null; n = n.getNextSibling()) {
+	    if(n.getNodeName().equals("Attribute")) {
+		Element e = (Element)n;
+		if(e.getAttribute("name").equals(name)) {
+		    if(attributes == null) {
+			attributes = new Element[1];
+			attributes[0] = e;
+		    }
+		    else {
+			Element temp[] = attributes;
+			attributes = new Element[temp.length + 1];
+			System.arraycopy(temp, 0, attributes, 0, temp.length);
+			attributes[temp.length] = e;
+			temp = null;
+		    }
+		}
+		e = null;
+	    }
+	}
+	return attributes;
+    }
+
+    /** Method to construct an elements description by retrieving the correct attribute.
+     * @param element the Element whose name we wish to retrieve
+     * @return a String which is the elements description, or an empty string if no description exists
+     * @see org.greenstone.gatherer.msm.MSMUtils#getElementAttribute
+     * @see org.greenstone.gatherer.util.StaticStrings#COMMENT_VALUE
+     * @see org.greenstone.gatherer.util.StaticStrings#DEFINITION_VALUE
+     * @see org.greenstone.gatherer.util.StaticStrings#EMPTY_STR
+     * @see org.greenstone.gatherer.util.StaticStrings#SPACE_CHARACTER
+     */
+    static public String getDescription(Element element) {
+	String language_code_str = Configuration.getLanguage();
+	StringBuffer description = new StringBuffer(StaticStrings.EMPTY_STR);
+	description.append(getElementAttribute(element, StaticStrings.DEFINITION_VALUE, language_code_str));
+	if(description.length() > 0) {
+	    description.append(StaticStrings.SPACE_CHARACTER);
+	}
+	description.append(getElementAttribute(element, StaticStrings.COMMENT_VALUE, language_code_str));
+	language_code_str = null;
+	return description.toString();
+    }
+
+    /** Retrieve the value for the requested attribute in the required language. Once again this method must be aware of the differences between the old metadata sets and the new multilingual optimized ones.
+     * @param element_element the Element whose attributes we are searching through
+     * @param attribute_name_str the name of the desired attribute as a String
+     * @param language_code_str the two letter code String indicating the desired language
+     * @see org.greenstone.gatherer.msm.MSMUtils#getValue
+     * @see org.greenstone.gatherer.msm.MSMUtils#isAttributeLanguageDependant
+     * @see org.greenstone.gatherer.util.StaticStrings#ATTRIBUTE_ELEMENT
+     * @see org.greenstone.gatherer.util.StaticStrings#CODE_ATTRIBUTE
+     * @see org.greenstone.gatherer.util.StaticStrings#EMPTY_STR
+     * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGE_ATTRIBUTE
+     * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGE_ELEMENT
+     * @see org.greenstone.gatherer.util.StaticStrings#NAME_ATTRIBUTE
+     */
+    static private String getElementAttribute(Element element_element, String attribute_name_str, String language_code_str) {
+	boolean found = false;
+	String result = StaticStrings.EMPTY_STR;
+	Document document = element_element.getOwnerDocument();
+	// Determine if the attribute is language specific
+	if(isAttributeLanguageDependant(document, attribute_name_str)) {
+	    NodeList language_elements = element_element.getElementsByTagName(StaticStrings.LANGUAGE_ELEMENT);
+	    for(int i = 0; !found && i < language_elements.getLength(); i++) {
+		Element language_element = (Element) language_elements.item(i);
+		if(language_element.getAttribute(StaticStrings.CODE_ATTRIBUTE).equals(language_code_str)) {
+		    NodeList attribute_elements = language_element.getElementsByTagName(StaticStrings.ATTRIBUTE_ELEMENT);
+		    for(int j = 0; !found && j < attribute_elements.getLength(); j++) {
+			Element attribute_element = (Element) attribute_elements.item(j);
+			if(attribute_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equals(attribute_name_str)) {
+			    found = true;
+			    result = MSMUtils.getValue(attribute_element);
+			}
+			attribute_element = null;
+		    }
+		    attribute_elements = null;
+		}
+		language_element = null;
+	    }
+	    language_elements = null;
+	}
+	else {
+	    boolean first_match = false;
+	    NodeList attribute_elements = element_element.getElementsByTagName(StaticStrings.ATTRIBUTE_ELEMENT);
+	    for(int k = 0; !found && k < attribute_elements.getLength(); k++) {
+		Element attribute_element = (Element) attribute_elements.item(k);
+		// We don't want to consider those attributes found inside language elements
+		if(attribute_element.getParentNode() == element_element) {
+		    ///ystem.err.println("First level");
+		    String target_name_str = attribute_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+		    String target_language_str = attribute_element.getAttribute(StaticStrings.LANGUAGE_ATTRIBUTE);
+		    ///ystem.err.println("Does " + target_name_str + " equal " + attribute_name_str + "?");
+		    if(attribute_name_str.equals(target_name_str)) {
+			///ystem.err.println("Does " + target_language_str + " equal " + language_code_str + "?");
+			if(language_code_str.equals(target_language_str)) {
+			    ///ystem.err.println("Perfect match!");
+			    found = true;
+			    result = MSMUtils.getValue(attribute_element);
+			}
+			else if((result == StaticStrings.EMPTY_STR || first_match) && isLegacyMDS(document)) {
+			    ///ystem.err.println("Legacy MDS");
+			    // Special case for old style documents, where the english match is good enough
+			    if(target_language_str.equals(StaticStrings.ENGLISH_LANGUAGE_STR)) {
+				///ystem.err.println("English plate.");
+				result = MSMUtils.getValue(attribute_element);
+			    }
+			    // Super special case where the first match is better than nothing
+			    else if(result == StaticStrings.EMPTY_STR && !first_match) {
+				///ystem.err.println("First match.");
+				first_match = true;
+				result = MSMUtils.getValue(attribute_element);
+			    }
+			}
+		    }
+		    target_language_str = null;
+		    target_name_str = null;
+		}
+		//else {
+		///ystem.err.println("Second level");
+		//}
+		attribute_element = null;
+	    }
+	    attribute_elements = null;
+
+	}
+	document = null;
+	return result;
+    }
+
+    /*************************************************************************/
+    /** Method to construct an elements fully qualified name. Note that this is different from a nodes identifier. Think of name as a short, unique reference to a metadata element, whereas identifier can be much longer, language specific and non-unique.
+     * @param element An <strong>Element</strong> whose name we are interested in.
+     * @return A <strong>String</strong> representing this given elements fully namespace qualified name.
+     */
+    static final public String getFullName(Element element) {
+	return getFullName(element, "");
+
+    }
+    /*************************************************************************/
+    /** Method to construct an elements fully qualified name. Note that this is different from a nodes identifier. Think of name as a short, unique reference to a metadata element, whereas identifier can be much longer, language specific and non-unique.
+     * @param element An <strong>Element</strong> whose name we are interested in.
+     * @return A <strong>String</strong> representing this given elements fully namespace qualified name.
+     */
+    static final public String getFullName(Element element, String namespace) {
+	StringBuffer name_buffer = new StringBuffer();
+	if(element == null) {
+	    return "Error";
+	}
+	// First get the root node.
+	Document document = element.getOwnerDocument();
+	Element root = document.getDocumentElement();
+	document = null;
+	// Retrieve this elements name
+	name_buffer.append(element.getAttribute("name"));
+	// Now we check if element has a parent node, other than root. If so we begin building up the full name
+	Element parent_element = (Element) element.getParentNode();
+	while(parent_element != null && parent_element != root) {
+	    name_buffer.insert(0, SF_SEP);
+	    name_buffer.insert(0, parent_element.getAttribute("name"));
+	    parent_element = (Element)parent_element.getParentNode();
+	}
+	parent_element = null;
+	// Finally insert the namespace and we are all done.
+	if(root != null) {
+	    namespace = root.getAttribute("namespace");
+	} 
+	root = null;
+	// If no root, or no namespace found, assume its extracted (at least then they can't edit it)
+	if(namespace == null || namespace.equals("")) {
+	    namespace = Utility.EXTRACTED_METADATA_NAMESPACE;
+	}
+	name_buffer.insert(0, NS_SEP);
+	name_buffer.insert(0, namespace);
+	namespace = null;
+	return name_buffer.toString();
+    } // static public String getFullName(Element element)
+
+    /** Method to construct an elements name (sic identifier) by retrieving the correct attribute, language specific.
+     * @param element the Element whose name we wish to retrieve
+     * @return a String which is the elements identifier, or an empty string if no identifier exists
+     * @see org.greenstone.gatherer.msm.MSMUtils#getElementAttribute
+     * @see org.greenstone.gatherer.util.StaticStrings#IDENTIFIER_VALUE
+     * @see org.greenstone.gatherer.util.StaticStrings#NAME_ATTRIBUTE
+     */
+    static final public String getIdentifier(Element element) {
+	String identifier = getElementAttribute(element, StaticStrings.IDENTIFIER_VALUE, Configuration.getLanguage());
+	// Failing the above we return the nodes name instead.
+	if(identifier == null || identifier.length() == 0) {
+	    identifier = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+	}
+	return identifier;
+    }
+
+    /** 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.
+     */ 
+    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;
+    }
+    /** Look for the occurances 'field' of the element and return it if found.
+     * @return An <i>int</i> which matches the number in the occurances attribute of the element, or 0 if no such attribute.
+     */
+    static final public int getOccurances(Element element) {
+	int count = 0;
+	String number = null;
+	if((number = element.getAttribute("occurances")) != null) {
+	    try {
+		count = Integer.parseInt(number);
+	    }
+	    catch(Exception error) {
+		count = 0;
+	    }
+	}
+	return count;
+    }	 
+    /** This method extracts the option list details, if any, from a certain element and stores them in an array ready to be passed as arguments to the <strong>Dictionary</strong>.
+     * @param element The <strong>Element</strong> whose option list we wish to view.
+     * @return A <strong>String[]</strong> containing the details of the option list.
+     * TODO implement.
+     * @see org.greenstone.gatherer.Dictionary
+     */
+    static final public String[] getOptionListDetails(Element element) {
+	return null;
+    }
+
+
+    /** This method extracts the structural details from a certain element and stores them in an array, all ready for passing to the <strong>Dictionary</strong>.
+     * @param element The <Strong>Element</strong> whose details we wish to gather.
+     * @return A <strong>String[]</strong> containing the structural details.
+     */
+    static final public String[] getStructuralDetails(MetadataSet mds, Element element) {
+	String details[] = new String[4];
+	//Element root = (Element)element.getParentNode();
+	//details[0] = root.getAttribute("name");
+	//details[1] = root.getAttribute("namespace");
+	details[0] = mds.getName();
+	details[1] = mds.getNamespace();
+	details[2] = getFullName(element);
+	details[3] = null;
+	// Get attributes
+	Vector attributes = new Vector();
+	for(Node n=element.getFirstChild(); n!=null; n=n.getNextSibling()) {
+	    if(n.getNodeName().equals("Attribute")) {
+		Element temp = (Element)n;
+		attributes.add(temp.getAttribute("name") + "=" + getValue(n));
+	    }
+	}
+	// Sort attributes
+	Collections.sort(attributes);
+	// Add attributes to details.
+	for(int i = 0; i < attributes.size(); i++) {
+	    if(details[3] == null) {
+		details[3] = "   " + attributes.get(i);
+	    }
+	    else {
+		details[3] = details[3] + "\n   " + attributes.get(i);
+	    }
+	}
+	return details;
+    }
+
+    /** Method to retrieve the value of a given node (not the assigned values tree!).
+     * @param element The <strong>Element</strong> whose value we wish to find.
+     * @return The value found as a <strong>String</strong>, or <i>null</i> if this element has no value.
+     */
+    static final public String getValue(Node element) {
+	// 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 traverse the given value tree, and build up a hashtable of mappings between the value path key names and the Subject nodes of the tree.
+     * @param current The root <strong>Node</strong> of a subtree of the AssignedValues tree.
+     * @param prefix The value path key <strong>String</string>, which shows the path from the root of the AssignedValue tree to <i>current</i>s parent using '\' as a separator.
+     * @param values A <strong>Hashtable</strong> containing the mapping discovered so far in our tree traversal.
+     */ 
+    static final private void getValueMappings(Node current, String prefix, Hashtable values) {
+	if(current != null) {
+	    String name = current.getNodeName();
+	    String new_prefix = prefix;
+	    // If we've found the outer layer of a new value, add it to our mapping
+	    if(name.equals("Subject")) {
+		Node value_node = getNodeFromNamed(current, "Value");
+		String value = getValue(value_node);
+		if(new_prefix != null) {
+		    new_prefix = new_prefix + "\\" + value;
+		}
+		else {
+		    new_prefix = value;
+		}
+		values.put(new_prefix, current);
+	    }
+	    if(name.equals("Subject") || name.equals("AssignedValues")) {
+		for(Node child = current.getFirstChild(); child != null;
+		    child = child.getNextSibling()) {
+		    getValueMappings(child, new_prefix, values);
+		}
+	    }
+	}
+    }
+    /** Parses the value tree template file.
+     * @return The Document parsed.
+     */
+    static final public Document getValueTreeTemplate() {
+	return Utility.parse(Utility.METADATA_VALUE_TEMPLATE, true);
+    }
+
+    /** Determine if the named attribute is language specific for this collection. This information is found in a DOM attribute of the document element, as a comma separated list of attribute names.
+     * @param document the Document for which we wish to check the language requirements
+     * @param attribute_name_str the name of the attribute we a testing as a String
+     * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGEDEPENDANT_ATTRIBUTE
+     */
+    static private boolean isAttributeLanguageDependant(Document document, String attribute_name_str) {
+	String language_specific_attributes = document.getDocumentElement().getAttribute(StaticStrings.LANGUAGEDEPENDANT_ATTRIBUTE).toLowerCase();
+	return language_specific_attributes.indexOf(attribute_name_str) != -1;
+    }
+
+    /** Determine if the given document is a legacy MDS or a new multilingual one. The easiest way to tell is whether there is a language_dependant attribute in the document element.
+     * @param document the Document to test
+     * @return true if this is an old mds, false otherwise
+     * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGEDEPENDANT_ATTRIBUTE
+     * @see org.greenstone.gatherer.util.StaticStrings#EMPTY_STR
+     */
+    static private boolean isLegacyMDS(Document document) {
+	///ystem.err.println("isLegacyMDS(): l_d = " + document.getDocumentElement().getAttribute(StaticStrings.LANGUAGEDEPENDANT_ATTRIBUTE));
+	return (document.getDocumentElement().getAttribute(StaticStrings.LANGUAGEDEPENDANT_ATTRIBUTE)).equals(StaticStrings.EMPTY_STR);
+    }
+
+    /** Method to compare two OptionsLists for equality.
+     * @param al A <strong>Node</strong> which represents an OptionList.
+     * @param bl The <strong>Node</strong> we wish to test against.
+     * @return A <i>boolean</i> which is <i>true</i> if the two option lists are equal, <i>false</i> otherwise.
+     * TODO Implementation
+     */
+    static final private boolean optionListsEqual(Node al, Node bl) {
+	// Compare the 'restricted' attribute of the two lists.
+	Element ae = (Element) al;
+	Element be = (Element) bl;
+	if(!ae.getAttribute("restricted").equals
+	   (be.getAttribute("restricted"))) {
+	    return false;
+	}
+	// Compare the Values under each list.
+	for(Node an = al.getFirstChild(); an != null; 
+	    an = an.getNextSibling()){
+	    if(an.getNodeName().equals("Value")) {
+		boolean matched = false;
+		for(Node bn = bl.getFirstChild(); bn != null && !matched;
+		    bn = bn.getNextSibling()) {
+		    if(bn.getNodeName().equals("Value")) {
+			matched = valuesEqual(an, bn);
+		    }
+		}
+		if(!matched) {
+		    return false;
+		}
+	    }
+	}
+	return true;
+    }
+
+    /** A method to remove a specific attribute element from an element. This attribute must match in name, language and in value before being removed. Note that this method supports both legacy and multilingual optimized versions of the mds.
+     * @param element_element the Element which represent the metadata element we are altering
+     * @param attribute_name_str the name of the attribute to remove as a String
+     * @param language_code_str the language code we must match as a String
+     * @param value_str the value String which also must match before we remove anything
+     * @return true if the desired attribute was successfully found and removed, false otherwise
+     * @see org.greenstone.gatherer.msm.MSMUtils#isAttributeLanguageDependant
+     * @see org.greenstone.gatherer.msm.MSMUtils#getValue(Node)
+     * @see org.greenstone.gatherer.util.StaticStrings#ATTRIBUTE_ELEMENT
+     * @see org.greenstone.gatherer.util.StaticStrings#CODE_ATTRIBUTE
+     * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGE_ATTRIBUTE
+     * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGE_ELEMENT
+     * @see org.greenstone.gatherer.util.StaticStrings#NAME_ATTRIBUTE
+     */
+    static public boolean removeElementAttribute(Element element_element, String attribute_name_str, String language_code_str, String value_str) {
+	// Multilingual Optimized version
+	// 1. Determine the if this is one of the language specific attributes
+	if(isAttributeLanguageDependant(element_element.getOwnerDocument(), attribute_name_str)) {
+	    // Retrieve the language elements, and determine the correct one
+	    NodeList language_elements = element_element.getElementsByTagName(StaticStrings.LANGUAGE_ELEMENT);
+	    for(int i = 0; i < language_elements.getLength(); i++) {
+		Element language_element = (Element) language_elements.item(i);
+		if(language_element.getAttribute(StaticStrings.CODE_ATTRIBUTE).equalsIgnoreCase(language_code_str)) {
+		    NodeList attribute_elements = language_element.getElementsByTagName(StaticStrings.ATTRIBUTE_ELEMENT);
+		    for(int j = 0; j < attribute_elements.getLength(); j++) {
+			Element attribute_element = (Element) attribute_elements.item(j);
+			String target_name_str = attribute_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
+			String target_value_str = MSMUtils.getValue(attribute_element);
+			if(attribute_name_str.equals(target_name_str) && value_str.equals(target_value_str)) {
+			    language_element.removeChild(attribute_element);
+			    if(attribute_elements.getLength() == 0) {
+				element_element.removeChild(language_element);
+			    }
+			    target_value_str = null;
+			    target_name_str = null;
+			    attribute_element = null;
+			    attribute_elements = null;
+			    language_element = null;
+			    language_elements = null;
+			    return true;
+			}
+			target_value_str = null;
+			target_name_str = null;
+			attribute_element = null;			
+		    }
+		    attribute_elements = null;
+		}
+		language_element = null;
+	    }
+	    language_elements = null;
+	    // Not found
+	    return false;
+	}
+	// Otherwise just use the old method
+
+	// Find the attribute to remove
+	NodeList attribute_elements = element_element.getElementsByTagName(StaticStrings.ATTRIBUTE_ELEMENT);
+	for (int k = 0; k < attribute_elements.getLength(); k++) {
+	    Element attribute_element = (Element) attribute_elements.item(k);
+	    // Remember to ignore any attributes that live within nested language elements
+	    if (attribute_element.getParentNode() == element_element && attribute_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equals(attribute_name_str) && attribute_element.getAttribute(StaticStrings.LANGUAGE_ATTRIBUTE).equalsIgnoreCase(language_code_str) && MSMUtils.getValue(attribute_element).equals(value_str)) {
+		// Match found, so remove the attribute node and return
+		element_element.removeChild(attribute_element);
+		attribute_element = null;
+		attribute_elements = null;
+		return true;
+	    }
+	    attribute_element = null;
+	}
+	attribute_elements = null;
+	// No match found
+	return false;
+    }
+
+    /** can only be used to set the english value */
+    static final public void setIdentifier(Node element, String value) {
+	// Get the 'identifier' Element
+	for(Node node = element.getFirstChild(); node != null;
+	    node = node.getNextSibling()) {
+	    if(node.getNodeName().equals("Attribute")) {
+		Element target = (Element)node;
+		if(target.getAttribute("name").equals("identifier") && target.getAttribute("language").equals("en")) {
+		    Node text = target.getFirstChild();
+		    text.setNodeValue(value);
+		    break;
+		}
+	    }
+	}
+    }
+    /** Set the value of the element attribute occurances.
+     * @param element The <strong>Element</strong> to change.
+     * @param value The value to change by as an <i>int</i>.
+     */
+    static final public void setOccurance(Element element, int value) {
+	Integer new_value = new Integer(getOccurances(element) + value);
+	element.setAttribute("occurances", new_value.toString());
+    }
+
+    /** 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
+     */
+    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));
+	}
+    }
+
+    /** This method also traverses the tree, but this one is used to gather all the values and aliases at once, and to 'prune' the tree if necessary.
+     * @param current The current root <strong>Node</strong> of this AssignedValues tree subtree.
+     * @param return_filter This <i>int</i> specifies what nodes from the tree should be returned, where;<br>VALUES  = return values only<br>ALIASES = return aliases only<br>BOTH    = return both values and aliases<br>NONE    = return nothing.
+     * @param remove_leaves A leaf node is a subject that contains no child subjects. If this <i>boolean</i> is set to <i>true</i> then the leaf nodes will be removed from the tree. 
+     * @return A <strong>Node[]</strong> containing whatever values or aliases have been found during the trees traversal.
+     */
+    static final public Node[] traverseTree(Node current, int return_filter, boolean remove_leaves) {
+	Node leaves[] = null;
+	String name = current.getNodeName();
+	if(name.equals("Value") && (return_filter == VALUES || return_filter == BOTH)) {
+	    leaves = ArrayTools.add(leaves, current);
+	}
+	else if(name.equals("Alias") && (return_filter == ALIASES || return_filter == BOTH)) {
+	    leaves = ArrayTools.add(leaves, current);
+	}
+	else if(name.equals("Subject")) {
+	    boolean has_subject_child = false;
+	    Node children[] = ArrayTools.nodeListToNodeArray(current.getChildNodes());
+	    for(int i = 0; i < children.length; i++) {
+		if(children[i].getNodeName().equals("Subject")) {
+		    has_subject_child = true;
+		}
+		leaves = ArrayTools.add(leaves, traverseTree(children[i], return_filter, remove_leaves));
+	    }
+	    if(!has_subject_child && remove_leaves) {
+		Node parent = current.getParentNode();
+		parent.removeChild(current);
+	    }
+	}
+	else if(name.equals("AssignedValues")) {
+	    Node children[] = ArrayTools.nodeListToNodeArray(current.getChildNodes());
+	    for(int i = 0; i < children.length; i++) {
+		leaves = ArrayTools.add(leaves, traverseTree(children[i], return_filter, remove_leaves));
+	    }
+	}															  
+	return leaves;
+    }
+
+    /** This method is used to systematically merge two AssignedValues tree. Both trees have their current values mapped, then the new tree is searched for key paths that don't exist in the current tree. If such a key is found, the Subject <strong>Node</strong> it maps to is retrieved and then imported and added to whatever was the closest available node (in terms of tree path) in the current tree.
+     * @param a_set The MetadataSet from which the Element a came from.
+     * @param a The Element at the root of the current AssignedValues tree.
+     * @param b_set The MetadataSet from which the Element b came from.
+     * @param b The root Element of the tree that is being merged.
+     * @return A <i>boolean</i> which is <i>true</i> if the trees merged without error, <i>false</i> otherwise.
+     */
+    static final public boolean updateValueTree(MetadataSet a_set, Element a, MetadataSet b_set, Element b) {
+	GValueModel avt = a_set.getValueTree(new ElementWrapper(a));
+	GValueModel bvt = b_set.getValueTree(new ElementWrapper(b));
+	// If neither element even has a value tree, we're all done.
+	if(avt == null && bvt == null) {
+	    avt = null;
+	    bvt = null;
+	    return true;
+	}
+	// If the new element has no value tree then nothing needs to be done.
+	else if(avt != null && bvt == null) {
+	    avt = null;
+	    bvt = null;
+	    return true;
+	}
+	// If only the new element has a value tree, then add all of its values
+	// immediately.
+	else if(avt == null && bvt != null) {
+	    a_set.addValueTree(new ElementWrapper(a), bvt);
+	    avt = null;
+	    bvt = null;
+	    return true;
+	}
+	// We have both trees for both elements, time to merge.
+	else {
+	    Document document = avt.getDocument();
+	    Hashtable a_map = new Hashtable();
+	    getValueMappings(document.getDocumentElement(), null, a_map);
+	    Hashtable b_map = new Hashtable();
+	    getValueMappings(bvt.getDocument().getDocumentElement(), null, b_map);
+				// For each new entry in b_map
+	    for(Enumeration b_keys = b_map.keys(); b_keys.hasMoreElements(); ) {
+		String b_key = (String)b_keys.nextElement();
+		// Test if there is already an entry in a_map.
+		if(!a_map.containsKey(b_key)) {
+		    // If not, search through a_map for the longest match.
+		    Node target = document.getDocumentElement();
+		    String last_match = null;
+		    for(Enumeration a_keys = a_map.keys(); 
+			a_keys.hasMoreElements(); ) {
+			String a_key = (String)a_keys.nextElement();
+			if(b_key.startsWith(a_key)) {
+			    if(last_match == null || a_key.length() > last_match.length()) {
+				last_match = a_key;
+				target = (Node)a_map.get(a_key);
+			    }
+			}
+			a_key = null;
+		    }
+		    // Now import the node at b_key and add it to target.
+		    Node subtree = (Node)b_map.get(b_key);
+		    subtree = document.importNode(subtree, true);
+		    // Find the node to insert before...
+		    String name = getValue(subtree);
+		    Node move = null;
+		    for(Node n = target.getFirstChild(); n != null && move == null; n = n.getNextSibling()) {
+			if(n.getNodeName().equals("Subject")) {
+			    if(name.compareTo(getValue(n)) <= 0) {
+				move = n;
+			    }
+			}
+		    }
+		    if(move == null) {
+			target.appendChild(subtree);
+		    }
+		    else {
+			target.insertBefore(subtree, move);
+		    }
+		    target = null;
+		    last_match = null;
+		    subtree = null;
+		    name = null;
+		    move = null;
+		}
+		b_key = null;
+	    }
+	    document = null;
+	    a_map = null;
+	    b_map = null;
+	    avt = null;
+	    bvt = null;
+	    return true;
+	}
+    }
+
+    /** Method to determine if two Value nodes are equal.
+     * @param av A <strong>Node</strong> representing a value.
+     * @param bv The <strong>Node</strong> we want to compare it to.
+     * @return A <i>boolean</i> which is <i>true</i> if the two value nodes are equal, <i>false</i> otherwise.
+     */
+    static final private boolean valuesEqual(Node av, Node bv) {
+	// Check we are comparing apples and apples...
+	if(av.getNodeName().equals("Value") && 
+	   bv.getNodeName().equals("Value")) {
+	    // Retrieve and then compare their text values.
+	    Node at = av.getFirstChild();
+	    Node bt = bv.getFirstChild();
+	    if(at.getNodeValue().equals(bt.getNodeValue())) {
+		return true;
+	    }
+	}
+	return false;
+    }
+
+    /** A comparator for sorting metadata element-value pairs into their standard order (elements) then alphabetical order (values). */
+//     static final private class MetadataComparator
+// 	implements Comparator {
+// 	/** Compares its two arguments for order. */
+// 	public int compare(Object o1, Object o2) {
+// 	    int result = 0;
+// 	    ElementWrapper e1 = null;
+// 	    ElementWrapper e2 = null;
+// 	    String v1 = null;
+// 	    String v2 = null;
+// 	    if(o1 instanceof Metadata && o2 instanceof Metadata) {
+// 		Metadata m1 = (Metadata) o1;
+// 		Metadata m2 = (Metadata) o2;
+// 		///ystem.err.println("MSMUtils.compare(" + m1 + ", " + m2 + ") = ");
+// 		e1 = m1.getElement();
+// 		e2 = m2.getElement();
+// 		v1 = m1.getValue().toLowerCase();
+// 		v2 = m2.getValue().toLowerCase();
+// 	    }
+// 	    else if(o1 instanceof ElementWrapper && o2 instanceof ElementWrapper) {
+// 		e1 = (ElementWrapper) o1;
+// 		e2 = (ElementWrapper) o2;
+// 	    }
+// 	    if(e1 != null && e2 != null) {
+// 		// First we compare the namespaces
+// 		result = e1.getNamespace().compareTo(e2.getNamespace());
+// 		if(result == 0 && Gatherer.c_man != null && Gatherer.c_man.ready() && e1.getNamespace() != null) {
+// 		    // Now, given both elements are in the same set, we compare the element ordering using methods in MetadataSet
+// 		    MetadataSet set = Gatherer.c_man.getCollection().msm.getSet(e1.getNamespace());
+// 		    ///ystem.err.print("MetadataSet.compare(" + e1 + ", " + e2 + ") = ");
+// 		    if(set != null) {
+// 			result = set.compare(e1.getElement(), e2.getElement());
+// 			///ystem.err.println(result);
+// 			if(result == 0 && v1 != null && v2 != null) {
+// 			    // Finally we compare the values alphabetically.
+// 			    result = v1.compareTo(v2);
+// 			}
+// 		    }
+// 		    else {
+// 			return 0;
+// 		    }
+// 		}
+// 	    }
+// 	    else {
+// 		result = o1.toString().compareTo(o2.toString());
+// 	    }
+// 	    ///ystem.err.println("Result: " + result);
+// 	    return result;
+// 	}
+          
+// 	/** Indicates whether some other object is "equal to" this Comparator. */
+// 	public boolean equals(Object obj) { 
+// 	    return compare(this, obj) == 0;
+// 	}
+//     }
+}
Index: /trunk/gli/src/org/greenstone/gatherer/gems/MetadataSet.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/MetadataSet.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/MetadataSet.java	(revision 8270)
@@ -0,0 +1,617 @@
+/**
+ *#########################################################################
+ *
+ * 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.gems;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import org.apache.xerces.dom.*;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.Dictionary;
+import org.greenstone.gatherer.util.StaticStrings;
+import org.greenstone.gatherer.util.Utility;
+import org.w3c.dom.*;
+/** An semi-data class to hold details about a loaded metadata set, it also provides some methods to manipulating the data within. 
+ * @author John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3b
+ */
+public class MetadataSet {
+    /** The <Strong>Document</strong> of the DOM model. */
+    private Document document = null;
+    /** The document <strong>Element</strong> of the DOM model. */
+    private Element  root     = null;
+    /** The <strong>File</strong> this metadata set was loaded from. */
+    private File     file     = null;
+    /** A mapping from metadata elements to the root element of the value trees for that element. */
+    private Hashtable value_trees = null;
+    /** The list of metadata elements which are, of course, children of the root node. */
+    private NodeList elements = null;
+
+    private String current_language_code;
+    /** The description of this metadata set. Cached as it takes more computation time. */
+    private String description = null;
+    /** The name of this metadata set. Cached as it takes more computation time. */
+    private String name = null;
+    /** An element of the tree pruning filter enumeration, that indicates all nodes in the tree should be retained. */
+    static final int ALL_VALUES    = 1;
+    /** An element of the tree pruning filter enumeration, that indicates only metadata Subject nodes or higher should remain after pruning. */
+    static final int SUBJECTS_ONLY = 2;
+    /** An element of the tree pruning filter enumeration, that indicates no value nodes i.e. the entire AssignedValues subtree, should remain after pruning. */
+    static final int NO_VALUES     = 3;
+
+    public MetadataSet(String metadata_template) {
+	
+	this.file = new File(metadata_template);
+	this.value_trees = new Hashtable();
+	this.document = Utility.parse(metadata_template, true);
+
+	init(true); // use class loader
+    }
+
+    /** Constructor.
+     * @param file The file the metadata set should be loaded from. 
+     */
+    public MetadataSet(File file) {
+	this.file = file;
+	this.value_trees = new Hashtable();
+	this.document = Utility.parse(file, false);
+
+	init(false); // don't use class loader
+    }
+
+    /** Copy constructor. 
+     * @param original The original metadata set to copy from.
+     */
+    public MetadataSet(MetadataSet original) {
+	this.value_trees = new Hashtable();
+	// We have to create a new document.
+	document = new DocumentImpl(original.getDocument().getDoctype());
+	root = (Element) document.importNode(original.getDocument().getDocumentElement(), true);
+	document.appendChild(root);
+	elements = root.getElementsByTagName("Element");
+	file = original.getFile();
+	// Now for each element read in its value tree if present.
+	for(int i = elements.getLength() - 1; i >= 0; i--) {
+	    ElementWrapper value_element_wrapper = new ElementWrapper((Element)elements.item(i));
+	    GValueModel value_tree = original.getValueTree(value_element_wrapper);
+	    Document value_document = value_tree.getDocument();
+	    Document value_document_copy = new DocumentImpl(value_document.getDoctype());
+	    Element value_element = value_document.getDocumentElement();
+	    Element value_element_copy = (Element) value_document_copy.importNode(value_element, true);
+	    value_document_copy.appendChild(value_element_copy);
+	    GValueModel value_tree_copy = new GValueModel(value_element_wrapper, value_document_copy);
+	    value_trees.put(value_element_wrapper, value_tree_copy);
+	}
+    }
+    /** Conditional copy constructor. 
+     * @param original The original metadata set to copy from.
+     * @param condition An <i>int</i> which matches one of the tree pruning filter types.
+     */
+    public MetadataSet(MetadataSet original, int condition) {
+	this(original); 
+	// Now based on condition, we may have to remove some nodes from
+	// this model.
+	switch(condition) {
+	case ALL_VALUES:
+	    // Do nothing.
+	    break;
+	case SUBJECTS_ONLY:
+	    // For each element retrieve its AssignedValues element.
+	    for(Enumeration keys = value_trees.keys(); keys.hasMoreElements(); ) {
+		ElementWrapper value_element = (ElementWrapper)keys.nextElement();
+		GValueModel value_tree = (GValueModel)value_trees.get(value_element);
+		Document value_tree_document = value_tree.getDocument();
+		Element value_tree_root_element = value_tree_document.getDocumentElement();
+		// Traverse tree and remove leaf nodes. 
+		MSMUtils.traverseTree(value_tree_root_element, MSMUtils.NONE, true);
+	    }
+	    break;
+	case NO_VALUES:
+	    // Remove assigned values trees.
+	    value_trees.clear();
+	    break;
+	}
+    }
+
+    /** Add a mds level attribute.
+     * @param name The name of the attribute to add as a <Strong>String</strong>.
+     * @param value The value as a <strong>String</strong>.
+     */
+    public void addAttribute(String name, String value) {
+	root.setAttribute(name, value);
+    }
+
+    /** Add a new default metadata element with the given name to this metadata set.
+     * @param name The name of this element as a <strong>String</strong>.
+     * @return An <strong>ElementWrapper</strong> around the newly created element or null if the element was not created.
+     */
+    public ElementWrapper addElement(String name, String language)
+    {
+	Text text = document.createTextNode(name);
+	Element identifier = document.createElementNS("","Attribute");
+	identifier.setAttribute("name","identifier");
+	identifier.setAttribute("language", language);
+	identifier.appendChild(text);
+	Element element = document.createElementNS("","Element");
+	element.setAttribute("name",name);
+	element.appendChild(identifier);
+	root.appendChild(element);
+	return new ElementWrapper(element);
+    }
+	
+    /** Method to add a new metadata element to this metadata set, if and only if the element is not already present. 
+     * @param others_element An <strong>Element</strong> we wish to add to this metadata set, that currently belongs to some other set.
+     * @param model A <strong>GValueModel</strong> value tree 
+     * @return <i>null</i> if the add is successful, otherwise a <strong>String</strong> containing an error message (phrase key).
+     */
+    public String addElement(Element others_element, GValueModel model) {
+	if(!containsElement(others_element.getAttribute("name"))) {
+	    // First get ownership of the new element, then add it.
+	    Element our_element = (Element)document.importNode(others_element, true);
+	    // add the value tree
+	    root.appendChild(our_element);
+	    if (model != null) {
+		addValueTree(new ElementWrapper(our_element), model);	
+	    }    
+	    return null;
+	}
+	else {
+	    return "MSMPrompt.Name_Exists";
+	}
+    }
+
+    /** Method to add a new metadata element with the specified new name to this metadata set, if and only if the name is not already in use. 
+     * @param others_element An <strong>Element</strong> we wish to add to this metadata set, that currently belongs to some other set.
+     * @param new_name The new name to be given this element, as a <strong>String</strong>.
+     * @param model A <strong>GValueModel</strong> value tree 
+     * @return <i>null</i> if the add is successful, otherwise a <strong>String</strong> containing an error message (phrase key).
+     */
+    public String addElement(Element others_element, String new_name, GValueModel model) {
+	if(!containsElement(new_name)) {
+	    // First get ownership of the new element, then add it.
+	    Element our_element = 
+		(Element) document.importNode(others_element, true);
+	    // Change name
+	    our_element.setAttribute("name", new_name);
+	    // we also want to change the english identifier of this element
+	    MSMUtils.setIdentifier(our_element, new_name);
+	    // Add it to teh set
+	    root.appendChild(our_element);
+	    // add the value tree
+	    if (model != null) {
+		addValueTree(new ElementWrapper(our_element), model);
+	    }
+	    return null;
+	}
+	else {
+	    return "MSMPrompt.Name_Exists";
+	}
+    }
+    /** Add a value tree to a given metadata element.
+     * @param element The <strong>ElementWrapper</strong> containing the element you wish to add a value tree for.
+     * @param model A <strong>GValueModel</strong> value tree 
+     */
+    public void addValueTree(ElementWrapper element, GValueModel model) {
+	///ystem.err.println("Adding value tree for " + element.toString());
+	value_trees.put(element, model);
+    }
+
+    public int compare(Element e1, Element e2) {
+	int result = 0;
+	// Check that they're not the same element.
+	if(e1 != e2) {
+	    int index_e1 = -1;
+	    int index_e2 = -1;
+	    // Locate the indexes for each element.
+	    for(int i = 0; i < elements.getLength(); i++) {
+		Node element = elements.item(i);
+		if(element == e1) {
+		    index_e1 = i;
+		}
+		if(element == e2) {
+		    index_e2 = i;
+		}
+	    }
+	    if(index_e1 < index_e2) {
+		result = -1;
+	    }
+	    else {
+		result = 1;
+	    }
+	}
+	return result;
+    }
+
+    /** A method to determine if this metadata set contains an element with a certain name (case sensitive).
+     * @param name A <strong>String</strong> which is the name of the element whose presence we are checking.
+     * @return A <i>boolean</i> which is <i>true</i> if the named element exists, <i>false</i> otherwise.
+     */
+    public boolean containsElement(String name) {
+	for(int i = 0; i < elements.getLength(); i++) {
+	    Element sibling = (Element) elements.item(i);
+	    String sibling_name = sibling.getAttribute("name");
+	    if(sibling_name.equals(name)) {
+		return true;
+	    }
+	}
+	return false;
+    }
+
+    public NamedNodeMap getAttributes() {
+	return root.getAttributes();
+    }
+
+    /** Method to retrieve the contact address of the metadata set creator.
+     * @return A <strong>String</strong> containing the address. 
+     */
+    /* private String getContact() {
+	return root.getAttribute("contact");
+	} */
+    /** Method to retrieve the name of the creator of this metadata set. 
+     * @return A <strong>String</strong> containing the name.
+     */
+    public String getCreator() {
+	return root.getAttribute("creator");
+    }
+    /** Method to retrieve the description of this metadata set. Note that this is language specific, so we determine the desired language from the Dictionary. If no such entry exists, first try returning the english version and failing that the first description found.
+     * @return The description as a <strong>String</strong>.
+     */
+    public String getDescription() {
+	if(current_language_code != null && !Configuration.getLanguage().equals(current_language_code)) {
+	    description = null;
+	}
+	if(description == null) {
+	    description = getAttribute(StaticStrings.DESCRIPTION_ELEMENT, Dictionary.get("MSM.No_Description"));
+	}
+	return description;
+    }
+
+    /** Method to retrieve the <strong>Document</strong> associated with this metadata set.
+     * @return The <strong>Document</strong> representing this metadata set.
+     */
+    public Document getDocument() {
+	return document;
+    }
+    /** Method to retrieve the metadata element indicated by an index.
+     * @param index An <i>int</i> specifying the required element.
+     * @return The <strong>Element</strong> at the index.
+     */
+    public Element getElement(int index) {
+	return (Element)elements.item(index);
+    }
+    /** This method is used to acquire a reference to the element which matches the given metadata. Note that this is not the same as <i>metadata.getElement()</i> as the reference returned by it may now be obsolete.
+     * @param metadata A <strong>Metadata</strong> object representing an element and value assignment.
+     * @return A 'live' reference to an <strong>Element</strong> which is the same as that referenced by the given metadata, or <i>null</i> if there is no such element.
+     */
+//     public Element getElement(Metadata metadata) {
+// 	return metadata.getElement().getElement();
+//     }
+    /** This method is used to acquire a reference to the element which has the name specified. Note that this is not the same as <i>metadata.getElement()</i> as the reference returned by it may now be obsolete.
+     * @param name A <strong>String</strong> stating the desired objects name.
+     * @return A 'live' reference to an <strong>Element</strong> which is the same as that referenced by the given metadata, or <i>null</i> if there is no such element.
+     */
+    public Element getElement(String name) {
+	// Strip any namespace.
+	while(name.indexOf(".") != -1 && !name.equals(".")) {
+	    name = name.substring(name.indexOf(".") + 1);
+	}
+	///ystem.err.println("Get element named " + name);
+	for(int i = 0; i < elements.getLength(); i++) {
+	    Element element = (Element) elements.item(i);
+	    ///ystem.err.println("Compare to: " + element.getAttribute("name"));
+	    if(element.getAttribute("name").equals(name)) {
+		return element;
+	    }
+	}
+	return null;
+    }
+
+    public Element getElement(Element parent_element, String name) {
+	DebugStream.println("Get element named " + name + " from " + parent_element.getAttribute("name"));
+	NodeList elements = parent_element.getElementsByTagName("Element");
+	for(int i = 0; i < elements.getLength(); i++) {
+	    Element element = (Element) elements.item(i);
+	    if(element.getAttribute("name").equals(name) && element.getParentNode() == parent_element) {
+		elements = null;
+		return element;
+	    }
+	}
+	elements = null;
+	return null;
+    }
+    /** Method to acquire a list of all the elements in this metadata set. 
+     * @return A <strong>NodeList</strong> containing all of this sets elements.
+     */
+    public NodeList getElements() {
+	return elements;
+    }
+
+    /** Method to retrieve a list of all the elements in this metadata set, sorted.
+     * @return A <strong>Vector</strong> containing all of the elements of this sets, sorted.
+     */
+//     public Vector getElementsSorted() {
+// 	Vector elements_list = new Vector();
+// 	for (int i = 0; i < elements.getLength(); i++) {
+// 	    elements_list.add(new ElementWrapper((Element) elements.item(i)));
+// 	}
+// 	Collections.sort(elements_list, MSMUtils.METADATA_COMPARATOR);
+// 	return elements_list;
+//     }
+
+    /** Method to retrieve the original file this metadata set was created from.
+     * @return A <strong>File</strong>.
+     */
+    public File getFile() {
+	return file;
+    }
+    /** Get the last changed attribute.
+     * @return Last changed as a <strong>String</strong>.
+     */
+    public String getLastChanged() {
+	return root.getAttribute("lastchanged");
+    }
+    /** Method to get this metadata sets name. Note that this is language specific, so we determine the desired language from the Dictionary. If no such entry exists, first try returning the english version and failing that the first name found.
+     * @return A <strong>String</strong> which contains its name.
+     */
+    public String getName() {
+	if(current_language_code != null && !Configuration.getLanguage().equals(current_language_code)) {
+	    name = null;
+	}
+	if(name == null) {
+	    name = getAttribute(StaticStrings.NAME_ELEMENT, Dictionary.get("MSM.No_Name"));
+	}
+	return name;
+    }
+    /** Method to retrieve this metadata sets namespace.
+     * @return The namespace as a <strong>String</strong>.
+     */
+    public String getNamespace() {
+	return root.getAttribute("namespace");
+    }
+    /** Method to retrieve the root element, i.e. the Document Element, of the DOM model behind this metadata set.
+     * @return An <strong>Element</strong> which is at the root of the modal.
+     */ 
+    public Element getRoot() {
+	return root;
+    }
+    /** Retrieve the value tree from this set that matches the given element.
+     * @param element The target <strong>ElementWrapper</strong>.
+     * @return A <strong>GValueModel</strong> value tree, or <i>null</i> if no such element or value tree.
+     */
+    public GValueModel getValueTree(ElementWrapper element) {
+	GValueModel value_tree = null;
+	// Stinking hashtable get doesn't use the overridden equals. So I'll do a loop, which should be pretty small ie O(n) for n metadata elements.
+	for(Enumeration keys = value_trees.keys(); keys.hasMoreElements(); ) {
+	    ElementWrapper sibling = (ElementWrapper) keys.nextElement();
+	    if(sibling.equals(element)) {
+		value_tree = (GValueModel) value_trees.get(sibling);
+		break;
+	    }
+	}
+	// If we've found no value tree, create a new one.
+	if(value_tree == null) {
+	    value_tree = new GValueModel(element);
+	    value_trees.put(element, value_tree);
+	}
+	return value_tree;
+    }
+    /** Remove a mds level attribute.
+     * @param name The name of the attribute to remove.
+     */
+    public void removeAttribute(String name) {
+	root.removeAttribute(name);
+    }
+    /** Method to remove the given element from this metadata set.
+     * @param element The <strong>Element</strong> to be removed.
+     */
+    public void removeElement(Element element) {
+	// we need to remove the value tree too!!
+	removeValueTree(new ElementWrapper(element));
+	root.removeChild(element);
+    }
+    /** Used to remove the value tree for a specific element.
+     * @param element The <strong>ElementWrapper</strong> whose tree you wish to remove.
+     * @return The <strong>GValueModel</strong> we just removed
+     */
+    public GValueModel removeValueTree(ElementWrapper element) {
+	for(Enumeration keys = value_trees.keys(); keys.hasMoreElements(); ) {
+	    ElementWrapper sibling = (ElementWrapper) keys.nextElement();
+	    if(sibling.equals(element)) {
+		GValueModel value_tree = (GValueModel) value_trees.get(sibling);
+		value_trees.remove(sibling);
+		return value_tree;
+	    }
+	}
+	return null;
+    }
+    /** Set one of the mds level attributes.
+     * @param name The attribute to change.
+     * @param value its new value.
+     */
+    public void setAttribute(String name, String value) {
+	root.setAttribute(name, value);
+    }
+    /** Once the metadata set has been saved to a different location, this is used to update the file parameter.
+     * @param file The new location of this metadata set <strong>File</strong>.
+     */
+    public void setFile(File file) {
+	this.file = file;
+    }
+
+    public void setName(String name) {
+	// Retrieve the name element. We look for the first english one.
+	Element name_element = null;
+	Element metadataset_element = document.getDocumentElement();
+	NodeList name_elements = metadataset_element.getElementsByTagName(Utility.NAME_ELEMENT);
+	for(int i = 0; i < name_elements.getLength(); i++) {
+	    Element possible_name_element = (Element) name_elements.item(i);
+	    if(possible_name_element.getAttribute(Utility.LANGUAGE_ATTRIBUTE).equals(Utility.ENGLISH_VALUE)) {
+		// Found it.
+		name_element = possible_name_element;
+	    }
+	}
+	// If there is none add one. Note that we can only add english metadata sets. Although others can edit them to add further names as necessary.
+	if(name_element == null) {
+	    name_element = document.createElement(Utility.NAME_ELEMENT);
+	    name_element.setAttribute(Utility.LANGUAGE_ATTRIBUTE, Utility.ENGLISH_VALUE);
+	    metadataset_element.insertBefore(name_element, metadataset_element.getFirstChild());
+	}
+	// Replace the text node
+	while(name_element.hasChildNodes()) {
+	    name_element.removeChild(name_element.getFirstChild());
+	}
+	name_element.appendChild(document.createTextNode(name));
+    }
+
+    /** Method to determine the number of elements in this set.
+     * @return An <i>int</i> specifying the element count.
+     */
+    public int size() {
+	return elements.getLength();
+    }
+    /** Method to translate this class into a meaningful string, which in this case is the metadata sets name.
+     * @return The metadata sets name as a <strong>String</strong>.
+     */
+    public String toString() {
+	String name = getName();
+	// If there is no given name, then use the namespace as there is garaunteed to be one of them.
+	if(name == null || name.length() == 0) {
+	    name = root.getAttribute("namespace");
+	}
+	// Append namespace
+	String namespace = root.getAttribute("namespace");
+	if(namespace == null || namespace.equals("")) {
+	    namespace = Utility.EXTRACTED_METADATA_NAMESPACE;
+	}
+	name = name + " (" + namespace + ")";
+	return name;
+    }
+
+    /** This method retrieves the required attribute from the Metadata Set, typically it's name or it's description. Note that this method is language dependant, and moreover supports both legacy metadata sets and the new sets optimized for multiple languages.
+     * @param element_name the name of the type of element the required information is in as a String
+     * @param default_string the value to return if no such element is found also as a String
+     * @see org.greenstone.gatherer.Configuration#getLanguage()
+     * @see org.greenstone.gatherer.msm.MSMUtils#getValue(Node)
+     * @see org.greenstone.gatherer.util.StaticStrings#CODE_ATTRIBUTE
+     * @see org.greenstone.gatherer.util.StaticStrings#SETLANGUAGE_ELEMENT
+     */
+    private String getAttribute(String element_name, String default_string) {
+	String result = null;
+
+	// Determine the language code.
+	current_language_code = Configuration.getLanguage();
+
+	///ystem.err.println("Searching for the " + element_name + " in " + current_language_code);
+
+	// New Metadata Set Format makes use of deferred-node-expansion to save memory - rather than create nodes for a name and description in each language, nodes which have potentially huge strings, we instead create simplier SETLANGUAGE nodes, and then only expand the one in the desired language. Of course if a user happens to change to every available language slightly more memory will be used than in the old method. For instance consider the DLS with 25 languages, each with a name node of 50 bytes and an descriptions of 500. Thus old style > 13750 bytes while new style < 600.
+	NodeList set_language_elements = document.getElementsByTagName(StaticStrings.SETLANGUAGE_ELEMENT);
+	for(int b = 0; b < set_language_elements.getLength(); b++) {
+	    Element set_language_element = (Element) set_language_elements.item(b);
+	    String code = set_language_element.getAttribute(StaticStrings.CODE_ATTRIBUTE).toLowerCase();
+	    if(code.equals(current_language_code)) {
+		NodeList specific_elements = set_language_element.getElementsByTagName(element_name);
+		if(specific_elements.getLength() > 0) {
+		    Element specific_element = (Element) specific_elements.item(0);
+		    result = MSMUtils.getValue(specific_element);
+		    specific_element = null;
+		}
+		specific_elements = null;
+	    }
+	    code = null;
+	    set_language_element = null;
+	}
+	set_language_elements = null;
+	// And we may be all done
+	if(result != null) {
+	    return result;
+	}
+
+	// Failing that we move on to an older style search - start by recovering all Name elements
+	NodeList possible_elements = document.getElementsByTagName(element_name);
+	// Iterate through the available names looking for the appropriate one. Also make note of the first name, then overwrite it with any english one.
+	boolean found = false;
+	for(int i = 0; !found && i < possible_elements.getLength(); i++) {
+	    Element possible_element = (Element) possible_elements.item(i);
+	    String possible_element_code = possible_element.getAttribute("language").toLowerCase();
+	    if(possible_element_code.equals(current_language_code) || name == null) {
+		result = MSMUtils.getValue(possible_element);
+		found = true;
+	    }
+	    possible_element_code = null;
+	    possible_element = null;
+	}
+	possible_elements = null;
+	// Failing all that set an error message
+	if(result == null) {
+	    result = default_string;
+	}
+	return result;
+    }
+
+    private void init(boolean use_classloader) {
+
+	if(this.document != null) {
+	    this.elements = document.getElementsByTagName("Element");
+	    this.root = document.getDocumentElement();
+	    // Now for each element read in its value tree if present.
+	    for(int i = elements.getLength() - 1; i >= 0; i--) {
+		ElementWrapper value_element = new ElementWrapper((Element)elements.item(i));
+		File value_file = new File(file.getParentFile(), value_element.getName() + ".mdv");
+		///ystem.err.println("Searching for " + value_file.getAbsolutePath());
+		if(value_file.exists()) {
+		    Document value_document;
+		    if (use_classloader) {
+			value_document = Utility.parse(value_file.toString(), true);
+		    }
+		    else {
+			value_document = Utility.parse(value_file, false);
+		    }
+		    if(value_document != null) {
+			value_trees.put(value_element, new GValueModel(value_element, value_document));
+		    }
+		    else {
+			DebugStream.println("Error! Missing mdv file: " + value_file.getAbsolutePath());
+		    }
+		}
+	    }
+	}
+	else {
+	    DebugStream.println("Error! Missing mds file: " + file.getAbsolutePath());
+	}
+    }
+}
Index: /trunk/gli/src/org/greenstone/gatherer/gems/MetadataSetManager.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/gems/MetadataSetManager.java	(revision 8270)
+++ /trunk/gli/src/org/greenstone/gatherer/gems/MetadataSetManager.java	(revision 8270)
@@ -0,0 +1,331 @@
+/**
+ *#########################################################################
+ *
+ * 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.gems;
+
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.filechooser.*;
+import javax.swing.tree.TreePath;
+import org.greenstone.gatherer.Configuration;
+import org.greenstone.gatherer.DebugStream;
+import org.greenstone.gatherer.util.Utility;
+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 John Thompson, Greenstone Digital Library, University of Waikato
+ * @version 2.3b
+ */
+public class MetadataSetManager {
+
+    /** The character used to separate name space from metadata element. */
+    static public char NS_SEP= '.';
+    /** The character used to separate subfields from metadata element. */
+    static public String SF_SEP= "#";
+
+    /** The name of the hidden, or system, metadata set. */
+    static final public String HIDDEN = "hidden";
+
+    /** A mapping from metadata namespace to metadata set. */
+    static private Hashtable mds_hashtable = new Hashtable();
+
+    /** The profiler is responsible for remembering what actions a user has requested when importing metadata, so as to prevent the user needlessly re-entering this information for each import. */
+    // public MSMProfiler profiler = null;
+
+
+    /** Constructor. */
+    public MetadataSetManager() {
+    }
+
+
+    public MetadataSet addSet(String namespace, String name) {
+	MetadataSet mds = new MetadataSet(Utility.METADATA_SET_TEMPLATE);
+	mds.setAttribute("creator", "The Greenstone Librarian Interface");
+	// Calculate lastchanged to right now on this machine by this user
+	String user_name = System.getProperty("user.name");
+	String machine_name = Utility.getMachineName();
+	mds.setAttribute("lastchanged", Utility.getDateString() + " - " + user_name + " on " + machine_name);
+	// And the remaining attributes.
+	//mds.setAttribute("name", name);
+	mds.setAttribute("namespace", namespace);
+	mds_hashtable.put(namespace, mds);
+	// Add the name element.
+	mds.setName(name);
+	// fireSetChanged(mds);
+	return mds;
+    }
+
+    /** Add a value tree to a given metadata element represented as a GValueModel
+     * @param model The <strong>GValueTree</strong> model
+     */
+    public void addValueTree(GValueModel model) {
+	ElementWrapper element = model.getElement();
+	String namespace = element.getNamespace();
+	MetadataSet mds = (MetadataSet) mds_hashtable.get(namespace);
+	if(mds != null) {
+	    mds.addValueTree(element, model);
+	}
+    }
+
+
+    /** Destructor. 
+     */
+    public void destroy() {
+	mds_hashtable.clear();
+    }
+
+
+    /** Used to get all the (non-hidden) elements in this manager.
+     * @return A Vector of ElementWrappers.
+     */
+    public Vector getElements() {
+	return getElements(false);
+    }
+
+    public Vector getElements(boolean all) {
+	return getElements(all, false);
+    }
+
+    /** Used to get all the elements in this manager.
+     * @param all <i>true</i> if all elements, including hidden, should be returned.
+     * @return A Vector of ElementWrappers.
+     */
+    public Vector getElements(boolean all, boolean force_extracted) {
+	Vector all_elements = new Vector();
+	for(Enumeration keys = mds_hashtable.keys(); keys.hasMoreElements(); ) {
+	    MetadataSet mds = (MetadataSet)mds_hashtable.get(keys.nextElement());
+	    if((!mds.getNamespace().equals(Utility.EXTRACTED_METADATA_NAMESPACE) && !mds.getNamespace().equals(HIDDEN)) 
+	       || (mds.getNamespace().equals(Utility.EXTRACTED_METADATA_NAMESPACE) && (Configuration.get("general.view_extracted_metadata", Configuration.COLLECTION_SPECIFIC) || force_extracted)) 
+	       || (mds.getNamespace().equals(Utility.EXTRACTED_METADATA_NAMESPACE) && mds.getNamespace().equals(HIDDEN) && all)) {
+		NodeList set_elements = mds.getElements();
+		///ystem.err.println("The set " + mds + " has " + set_elements.getLength() + " elements.");
+		for(int i = 0; i < set_elements.getLength(); i++) {
+		    Element raw_element = (Element)set_elements.item(i);
+		    ElementWrapper element = new ElementWrapper(raw_element);
+		    // For now we do not add subfield elements and their parents, just the subfields.
+		    NodeList child_elements = raw_element.getElementsByTagName("Element");
+		    if(child_elements.getLength() == 0) {
+			all_elements.add(element);
+		    }
+		}
+	    }
+	}
+	Collections.sort(all_elements);
+	return all_elements;
+    }
+
+    /** Retrieve a metadata element by its index.
+     * @param index The specified index as an int.
+     * @return An ElementWrapper containing the specied element, or <i>null</i> is no such element exists.
+     */
+    public ElementWrapper getElement(int index) {
+	Vector elements = getElements(false);
+	ElementWrapper result = null;
+	if(0 <= index && index < elements.size()) {
+	    result = (ElementWrapper) elements.get(index);
+	}
+	return result;
+    }
+    /** Retrieve a metadata element by looking at the current metadata element. Note that this 'index' element may now be disconnected from the DOM model, so we have to reload the target element by the string method.
+     * @param element The possibly out-of-data MetadataElement.
+     * @return An ElementWrapper containing the specied element, or <i>null</i> is no such element exists.
+     */
+    public ElementWrapper getElement(ElementWrapper element) {
+	return getElement(element.toString());
+    }
+    /** Retrieve a metadata element by its fully qualified name.
+     * @param name The elements name as a String.
+     * @return An ElementWrapper containing the specied element, or <i>null</i> is no such element exists.
+     */
+    public ElementWrapper getElement(String name) {
+	return getElement(name, false);
+    }
+
+    public ElementWrapper getElement(String name, boolean perfect) {
+	///ystem.err.println("Retrieve element " + name);
+	if(name == null) {
+	    ///ystem.err.println("No name!");
+	    return null;
+	}
+	ElementWrapper result = null;
+	MetadataSet set = null;
+	String element = null;
+	// First we seperate off what set it is in, where we have '<set><namespace_separator><element>'.
+	if(name.indexOf(NS_SEP) != -1) {
+	    String namespace = name.substring(0, name.indexOf(NS_SEP));
+	    // Retrieve the correct set if possible.
+	    set = (MetadataSet)mds_hashtable.get(namespace);
+	    namespace = null;
+	    // Now retrieve the element name.
+	    element = name.substring(name.indexOf(NS_SEP) + 1);
+	}
+	// If we are looking for a perfect match, we can assume that no namespace means extracted metadata
+	else if(!perfect) {
+	    // No namespace so assume that its extracted metadata.
+	    set = (MetadataSet)mds_hashtable.get(Utility.EXTRACTED_METADATA_NAMESPACE);
+	    element = name;
+	}
+	if(set != null) {
+	    // Now we have a set we are ready to locate the requested element. Break the remaining element name down by the subfield separator, attempting to retrieve the element indicated by each step.
+	    if(element.indexOf(SF_SEP) != -1) {
+		StringTokenizer tokenizer = new StringTokenizer(element, SF_SEP);
+		// Has to be at least two tokens
+		if(tokenizer.countTokens() >= 2) {
+		    Element current_element = set.getElement(tokenizer.nextToken());
+		    while(current_element != null && tokenizer.hasMoreTokens()) {
+			current_element = set.getElement(current_element, tokenizer.nextToken());
+		    }
+		    if(current_element != null) {
+			result = new ElementWrapper(current_element);
+			current_element = null;
+		    }
+		}
+		tokenizer = null;
+	    }
+	    // No subfields - much easier.
+	    if(result == null) {
+		///ystem.err.print("Trying to match element " + element +"?");
+		Element temp = set.getElement(element);
+		if(temp != null) {
+		    result = new ElementWrapper(temp);
+		}
+		temp = null;
+	    }
+	    set = null;
+	}
+	element = null;
+	return result;
+    }
+    /** Retrieve a certain named element from a certain named set.
+     * @param set The metadata set whose element you want.
+     * @param name The name of the element.
+     * @return An ElementWrapper around the requested element, or null if no such set or element.
+     */
+    public ElementWrapper getElement(String set, String name) {
+	if(mds_hashtable.containsKey(set)) {
+	    MetadataSet temp = (MetadataSet)mds_hashtable.get(set);
+	    return new ElementWrapper(temp.getElement(name));
+	}
+	return null;
+    }
+
+    /** Retrieve the named metadata set.
+     * @param name The sets name as a String.
+     * @return The MetadataSet as named, or null if no such set.
+     */
+    public MetadataSet getSet(String name) {
+	if(mds_hashtable.containsKey(name)) {
+	    return (MetadataSet) mds_hashtable.get(name);
+	}
+// 	else if(name.equals(HIDDEN)) {
+// 	    return createHidden();
+// 	}
+	return null;
+    }
+
+    /** Method to retrieve all of the metadata sets loaded in this collection. 
+     * @return A Vector of metadata sets.
+     */
+    public Vector getSets() {
+	return getSets(true);
+    }
+
+    public Vector getSets(boolean include_greenstone_extracted) {
+	Vector result = new Vector();
+	for(Enumeration keys = mds_hashtable.keys(); keys.hasMoreElements(); ) {
+	    MetadataSet set = (MetadataSet)mds_hashtable.get(keys.nextElement());
+	    if(!set.getNamespace().equals(HIDDEN) && (include_greenstone_extracted || !set.getNamespace().equals(Utility.EXTRACTED_METADATA_NAMESPACE))) {
+		result.add(set);
+	    }
+	}
+	return result;
+    }
+
+
+    /** Get the value tree that matches the given element.
+     * @param element The ElementWrapper representing the element.
+     * @return The GValueModel representing the value tree or null.
+     */
+    public GValueModel getValueTree(ElementWrapper element) {
+	GValueModel value_tree = null;
+	if(element != null) {
+	    String namespace = element.getNamespace();
+	    if(namespace.length() > 0) {
+		MetadataSet mds = (MetadataSet) mds_hashtable.get(namespace);
+		if(mds != null) {
+		    value_tree = mds.getValueTree(element);
+		}
+	    }
+	}
+	return value_tree;
+    }
+
+
+    public MetadataSet loadMetadataSet(File metadata_set_file)
+    {
+	System.err.println("Loading metadata set file " + metadata_set_file + "...");
+	MetadataSet metadata_set = new MetadataSet(metadata_set_file);
+	mds_hashtable.put(metadata_set.getNamespace(), metadata_set);
+	return metadata_set;
+    }
+
+
+    public void removeElement(ElementWrapper element) {
+	// Retrieve the metadata set this element belongs to.
+	String namespace = element.getNamespace();
+	MetadataSet set = (MetadataSet) mds_hashtable.get(namespace);
+	if(set != null) {
+	    // Bugger. Get the old name -before- we remove the element from the set.
+	    String old_name = element.toString();
+	    // Remove the element.
+	    set.removeElement(element.getElement());
+	}
+	else {
+	    ///ystem.err.println("no such set " + namespace);
+	}
+	// No such set. No such element.
+    }
+
+
+    public void removeSet(MetadataSet set) {
+	mds_hashtable.remove(set.getNamespace());
+	// fireSetChanged(set);
+    }
+}
