/** *######################################################################### * * 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.msm; 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.Gatherer; import org.greenstone.gatherer.valuetree.GValueModel; import org.greenstone.gatherer.valuetree.GValueNode; 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 Document of the DOM model. */ private Document document = null; /** The document Element of the DOM model. */ private Element root = null; /** The File 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 int 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 String. * @param value The value as a String. */ 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 String. * @return An ElementWrapper 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 Element we wish to add to this metadata set, that currently belongs to some other set. * @param model A GValueModel value tree * @return null if the add is successful, otherwise a String 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 Element 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 String. * @param model A GValueModel value tree * @return null if the add is successful, otherwise a String 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 ElementWrapper containing the element you wish to add a value tree for. * @param model A GValueModel 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 String which is the name of the element whose presence we are checking. * @return A boolean which is true if the named element exists, false 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 String containing the address. */ /* private String getContact() { return root.getAttribute("contact"); } */ /** Method to retrieve the name of the creator of this metadata set. * @return A String 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 String. */ 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 Document associated with this metadata set. * @return The Document representing this metadata set. */ public Document getDocument() { return document; } /** Method to retrieve the metadata element indicated by an index. * @param index An int specifying the required element. * @return The Element 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 metadata.getElement() as the reference returned by it may now be obsolete. * @param metadata A Metadata object representing an element and value assignment. * @return A 'live' reference to an Element which is the same as that referenced by the given metadata, or null 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 metadata.getElement() as the reference returned by it may now be obsolete. * @param name A String stating the desired objects name. * @return A 'live' reference to an Element which is the same as that referenced by the given metadata, or null 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 NodeList 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 Vector 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 File. */ public File getFile() { return file; } /** Get the last changed attribute. * @return Last changed as a String. */ 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 String 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 String. */ 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 Element 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 ElementWrapper. * @return A GValueModel value tree, or null 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 Element 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 ElementWrapper whose tree you wish to remove. * @return The GValueModel 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 File. */ 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 int 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 String. */ 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.Gatherer#config * @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()); } } }