/** *######################################################################### * * 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.util.*; import javax.swing.*; import javax.swing.filechooser.*; import javax.swing.tree.TreePath; import org.greenstone.gatherer.Configuration; import org.greenstone.gatherer.Dictionary; import org.greenstone.gatherer.Gatherer; import org.greenstone.gatherer.cdm.CommandTokenizer; import org.greenstone.gatherer.file.FileNode; import org.greenstone.gatherer.gui.MetaEditPrompt; import org.greenstone.gatherer.mem.MetadataEditorManager; import org.greenstone.gatherer.msm.Declarations; import org.greenstone.gatherer.msm.MDSFileFilter; import org.greenstone.gatherer.msm.MetadataSet; import org.greenstone.gatherer.msm.MSMAction; import org.greenstone.gatherer.msm.MSMEvent; import org.greenstone.gatherer.msm.MSMPrompt; import org.greenstone.gatherer.msm.MSMUtils; import org.greenstone.gatherer.valuetree.GValueModel; import org.greenstone.gatherer.valuetree.GValueNode; 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 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 class responsible for creating and maintaining all the visual components of the metadata management package. */ public MSMPrompt prompt = null; /** 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; /** Records all of the changes made to metadata as part of the current metadata change. Entries map from a particular FileNode to an ArrayList of the modified metadata for that node. Not to be confused with the undo managers idea of undo which records a list of all metadata changes requested. */ private HashMap undo_buffer = new HashMap(); /** The loader responsible for sequentially loading and attempting to use all registered metadata parsers to extract existing metadata from new files. */ private ExistingMetadataLoader loader = null; /** Specialized parser for parsing GreenstoneDirectoryMetadata files, which not only caches entries, but also breaks up massive metadata.xml files into reasonable sizes. */ private MetadataXMLFileParser gdm_parser = null; /** A list of classes who are interested in changes to the loaded metadata sets. */ private Vector listeners = null; /** Constructor. */ public MetadataSetManager() { this.gdm_parser = new MetadataXMLFileParser(); this.listeners = new Vector(); this.loader = new ExistingMetadataLoader(); loadProfiler(); this.prompt = new MSMPrompt(this); } /** Attach a piece of metadata to a record or records, ensuring the value tree is built properly, and correct messaging fired. * @param id * @param records A FileNode[] of records, or directories, to add the specified metadata to. * @param element The metadata element, contained within an ElementWrapper to base metadata on. * @param value_str The value to assign to the metadata as a String. */ public Metadata addMetadata(long id, FileNode records[], ElementWrapper element, String value_str) { Metadata metadata = null; if (records.length == 1) { metadata = addMetadata(id, records, element, value_str, MetaEditPrompt.ACCUMULATE_ALL); } else { metadata = addMetadata(id, records, element, value_str, MetaEditPrompt.CONFIRM); } return metadata; } public Metadata addMetadata(long id, FileNode records[], ElementWrapper element, String value_str, int action) { // Retrieve the appropriate value node from the value tree for this element, creating it if necessary. GValueModel model = getValueTree(element); GValueNode value = null; if(model != null) { value = model.addValue(value_str); // Only adds if not already present, otherwise just returns existing node. } else { value = new GValueNode(element.toString(), value_str); } // Create new metadata. Metadata metadata = new Metadata(element, value); // Reset the undo buffer. undo_buffer.clear(); // Assign to records. Note that we must watch for responses from the user prompts, and cease loop if break signalled. // Now add the metadata to each selected file node. for(int i = 0; action != MetaEditPrompt.CANCEL && i < records.length; i++) { action = addMetadata(id, records[i], metadata, action, (records.length > 1)); } // If we were cancelled we should undo any changes so far if(action == MetaEditPrompt.CANCEL) { for(Iterator keys = undo_buffer.keySet().iterator(); keys.hasNext(); ) { FileNode record = (FileNode) keys.next(); undoAdd(id, record); } } return metadata; } /** Adds a metadata set listener to this set, if it isn't alreay listening. * @param listener The new MSMListener. */ public void addMSMListener(MSMListener listener) { if(!listeners.contains(listener)) { listeners.add(listener); } } 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 GValueTree 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. * @see org.greenstone.gatherer.msm.MSMProfiler */ public void destroy() { mds_hashtable.clear(); profiler.destroy(); profiler = null; } /** Method called to open a metadata set editing window. * @return A boolean indicating if the edit was successful. */ public boolean editMDS(MetadataSet set, int action) { MetadataEditorManager mem = new MetadataEditorManager(set, action); mem.dispose(); mem = null; return true; } /** This method is called to export a metadata set. First a prompt is displayed to gather necessary details such as which metadata set to export, where to export it to and what conditions should be applied when exporting. Once this information is gathered the static method exportMDS() is called with the appropriate output stream. * @return A boolean which is true if the metadata set has been exported successfully, false otherwise. */ public boolean exportMDS() { ExportMDSPrompt emdsp = new ExportMDSPrompt(this, true); int result = emdsp.display(); MetadataSet set = emdsp.getSelectedSet(); if (result == ExportMDSPrompt.EXPORT && set != null) { File file = emdsp.getSelectedFile(); MetadataSet set_copy = new MetadataSet(set, emdsp.getSelectedCondition()); try { file.getParentFile().mkdirs(); Utility.export(set_copy.getDocument(), file); // Now for each element attempt to save its value tree. NodeList elements = set_copy.getElements(); for(int i = elements.getLength() - 1; i >= 0; i--) { ElementWrapper value_element = new ElementWrapper((Element)elements.item(i)); GValueModel value_tree = set_copy.getValueTree(value_element); if(value_tree != null) { File value_file = new File(file.getParentFile(), value_element.getName() + ".mdv"); ///ystem.err.println("Saving value file: " + value_file.toString()); Utility.export(value_tree.getDocument(), value_file); } } return true; } catch (Exception error) { error.printStackTrace(); } } emdsp.dispose(); emdsp = null; return false; } /** Fire an element changed message off to all registered listeners. * @param event An MSMEvent detailing the change. */ public void fireElementChanged(MSMEvent event) { // Then send it to all the listeners. for(int i = 0; i < listeners.size(); i++) { ((MSMListener)listeners.get(i)).elementChanged(event); } } /** Fire a metadata value changed message, whose id is to be generated now, and whose action code is -1, off to all registered listeners. */ public void fireMetadataChanged(FileNode node, Metadata old_data, Metadata new_data) { fireMetadataChanged(System.currentTimeMillis(), node, old_data, new_data, -1); } /** Fire a metadata value changed message, whose id is to be generated now, off to all registered listeners. */ public void fireMetadataChanged(FileNode node, Metadata old_data, Metadata new_data, int action) { fireMetadataChanged(System.currentTimeMillis(), node, old_data, new_data); } /** Fire a metadata value changed message off to all registered listeners. */ public void fireMetadataChanged(long id, FileNode node, Metadata old_data, Metadata new_data) { fireMetadataChanged(id, node, old_data, new_data, -1); } public void fireMetadataChanged(long id, FileNode node, Metadata old_data, Metadata new_data, int action) { if(old_data != null) { old_data.getElement().dec(); } if(new_data != null) { new_data.getElement().inc(); } ///ystem.err.println("Metadata changed: " + record + " > '" + old_data + "' -> '" + new_data + "'"); // Create a new MSMEvent based on the record. MSMEvent event = new MSMEvent(this, id, node, old_data, new_data, action); fireMetadataChanged(event); event = null; } public void fireMetadataChanged(MSMEvent event) { // Then send it to all the listeners. for(int i = 0; i < listeners.size(); i++) { ((MSMListener)listeners.get(i)).metadataChanged(event); } } /** Fire a metadata set changed message off to all registered listeners. * @param set The MetadataSet thats changed. */ public void fireSetChanged(MetadataSet set) { // Create a new MSMEvent, with a MSMAction containing only the new set. MSMEvent event = new MSMEvent(this, 0L, new MSMAction(set.toString(), null, -1, null)); // Then send it to all the listeners. for(int i = 0; i < listeners.size(); i++) { ((MSMListener)listeners.get(i)).setChanged(event); } } /** Called whenever the value tree associated with an element changes significantly. * @param element The metadata element whose value tree has changed, as an ElementWrapper. */ public void fireValueChanged(ElementWrapper element, GValueModel old_model, GValueModel new_model) { // Create a new MSMEvent based on the element wrapper. MSMEvent event = new MSMEvent(this, 0L, element, old_model, new_model); // Then send it to all the listeners. for(int i = 0; i < listeners.size(); i++) { ((MSMListener)listeners.get(i)).valueChanged(event); } } /** Builds a list of elements that have been assigned as metadata in this collection. We go through all of the elements, looking for elements whose occurances are greater than 0. A convenience call to the version with one parameter. * @return A Vector of assigned elements. */ public Vector getAssignedElements() { return getAssignedElements(false); } /** Builds a list of elements that have been assigned as metadata in this collection. We go through all of the elements, looking for elements whose occurances are greater than 0. * @param hierarchy_only true to only return those elements that are both assigned and have hierarchical value trees, false for just assignments. * @return A Vector of assigned elements. */ public Vector getAssignedElements(boolean hierarchy_only) { Vector elements = new Vector(); for(Enumeration keys = mds_hashtable.keys(); keys.hasMoreElements(); ) { MetadataSet mds = (MetadataSet)mds_hashtable.get(keys.nextElement()); if(!mds.getNamespace().equals(HIDDEN)) { NodeList set_elements = mds.getElements(); for(int i = 0; i < set_elements.getLength(); i++) { ElementWrapper element = new ElementWrapper((Element)set_elements.item(i)); elements.add(element); } } } Collections.sort(elements); for(int i = elements.size(); i != 0; i--) { ElementWrapper element = (ElementWrapper) elements.get(i - 1); if(element.getOccurances() == 0 && element.getNamespace().length() > 0 && !element.getNamespace().equals(Utility.EXTRACTED_METADATA_NAMESPACE)) { elements.remove(element); } else if(hierarchy_only) { GValueModel model = getValueTree(element); if(!model.isHierarchy()) { elements.remove(element); } } } return elements; } /** 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 true 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) && (Gatherer.config.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 null 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 null 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 null 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 ''. if(name.indexOf(MSMUtils.NS_SEP) != -1) { String namespace = name.substring(0, name.indexOf(MSMUtils.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(MSMUtils.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(MSMUtils.SF_SEP) != -1) { StringTokenizer tokenizer = new StringTokenizer(element, MSMUtils.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; } /** Find the total number of elements loaded. * @return The element count as an int. */ public int getSize() { int count = 0; if(mds_hashtable.size() > 0) { for(Enumeration keys = mds_hashtable.keys(); keys.hasMoreElements();) { MetadataSet mds = (MetadataSet)mds_hashtable.get(keys.nextElement()); if(mds.getNamespace().equals(HIDDEN)) { count = count + mds.size(); } } } return count; } /** 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 boolean importMDS(File mds_file, boolean user_driven) { // 1. Parse the new file. MetadataSet mds_new = new MetadataSet(mds_file); if(user_driven) { // Display a prompt asking how much of the value structure the user wishes to import. // ...but only if there are some MDV files to read values from FilenameFilter mdv_filter = new MDVFilenameFilter(mds_new.getNamespace()); File[] mdv_files = mds_file.getParentFile().listFiles(mdv_filter); if (mdv_files.length > 0) { ExportMDSPrompt imdsp = new ExportMDSPrompt(this, false); int result = imdsp.display(); if(result == ExportMDSPrompt.EXPORT) { // Here export means the user didn't cancel. switch(imdsp.getSelectedCondition()) { case MetadataSet.NO_VALUES: mds_new = new MetadataSet(mds_new, MetadataSet.NO_VALUES); break; case MetadataSet.SUBJECTS_ONLY: mds_new = new MetadataSet(mds_new, MetadataSet.SUBJECTS_ONLY); break; default: // ALL_VALUES // Don't do anything. } } else { mds_new = null; } imdsp.dispose(); imdsp = null; } } // Carry on importing the new collection if(mds_new != null && mds_new.getDocument() != null) { String family = mds_new.getNamespace(); // 2. See if we have another metadata set of the same family // already. If so retrieve it and merge. boolean matched = false; for(Enumeration keys = mds_hashtable.keys(); keys.hasMoreElements();) { String key = (String)keys.nextElement(); if(key.equals(family)) { matched = true; MetadataSet mds_cur = (MetadataSet)mds_hashtable.get(key); ///ystem.err.println("Merging " + mds_new + " into " + mds_cur); MergeTask task = new MergeTask(mds_cur, mds_new); task.start(); } } if(!matched) { ///ystem.err.println("Mapping " + family + " to " + mds_new); mds_hashtable.put(family, mds_new); } fireSetChanged(mds_new); return true; } // else we cancelled for some reason. return false; } private class MergeTask extends Thread { MetadataSet mds_cur; MetadataSet mds_new; MergeTask(MetadataSet mds_cur, MetadataSet mds_new) { this.mds_cur = mds_cur; this.mds_new = mds_new; } public void run() { mergeMDS(mds_cur, mds_new); // Fire setChanged() message. fireSetChanged(mds_new); } } /** Accepts .mdv files for a certain metadata set. */ private class MDVFilenameFilter implements FilenameFilter { private String mds_namespace = null; public MDVFilenameFilter(String mds_namespace) { this.mds_namespace = mds_namespace.toLowerCase(); } public boolean accept(File dir, String name) { String copy = name.toLowerCase(); if (copy.startsWith(mds_namespace) && copy.endsWith(".mdv")) { return true; } return false; } } /** This method reloads all of the metadata sets that have been marked as included in this collection by entries in the collection configuration file. */ public void load() { File source = new File(Gatherer.c_man.getCollectionMetadata()); File files[] = source.listFiles(); for(int i = 0; files != null && i < files.length; i++) { if(files[i].getName().endsWith(".mds")) { importMDS(files[i], false); } } // If no current 'hidden' metadata set exists, create one. if(getSet(HIDDEN) == null) { createHidden(); } } /** This method takes two metadata sets, the current one and a new one, and merges them. This merge takes place at an element level falling to lower levels as necessary (using mergeMDE() to merge elements and mergeMDV() to merge value trees. * @param mds_cur The currently loaded MetadataSet. * @param mds_new A new MetadataSet you wish to merge in. * @return A boolean with value true indicating if the merge was successful, otherwise false if errors were detected. */ public boolean mergeMDS(MetadataSet mds_cur, MetadataSet mds_new) { // For a super quick check for equivelent trees, we compare the last changed values. if(mds_cur.getLastChanged().equals(mds_new.getLastChanged())) { // Exactly the same. Nothing to change. return true; } // Show initial progress prompt. prompt.startMerge(mds_new.size()); boolean cancel = false; // For each element in the new set for(int i = 0; !cancel && i < mds_new.size(); i++) { boolean cont = false; Element mde_new = mds_new.getElement(i); GValueModel mde_values = mds_new.getValueTree(new ElementWrapper(mde_new)); // See if the element already exists in the current set Element mde_cur = mds_cur.getElement(mde_new.getAttribute("name")); int option = Declarations.NO_ACTION; while(!cont && !cancel) { // We may be dealing with a brand new element, or possibly a // renamed one. if(mde_cur == null) { // Provide merge, rename and skip options. option = prompt.mDSPrompt(mds_cur, null, mds_new, mde_new); } else { // If the two elements have equal structure we only have // to worry about merging the values. if(MSMUtils.elementsEqual(mds_cur, mde_cur, mds_new, mde_new, false)) { cancel = !mergeMDV(mds_cur, mde_cur, mds_new, mde_new); cont = true; } else { // Provide add, merge and skip options. option = prompt.mDSPrompt(mds_cur, mde_cur, mds_new, mde_new); } } String reason = null; switch(option) { case Declarations.ADD: // Only available to brand new elements, this options // simply adds the element to the set. reason = mds_cur.addElement(mde_new, mde_values); if(reason == null) { cont = true; } else { prompt.addFailed(mde_new, reason); cont = false; } break; case Declarations.CANCEL: cancel = true; cont = true; break; case Declarations.FORCE_MERGE: // If the mde_cur is null, that means the users has asked // to merge but hasn't choosen any element to merge with. // Make the user select an element. mde_cur = prompt.selectElement(mds_cur); case Declarations.MERGE: // This case in turn calls the mergeMDE method to perform // the actual merging of the Elements. if(mde_cur != null) { cancel = !mergeMDE(mds_cur, mde_cur, mds_new, mde_new); } cont = true; break; case Declarations.RENAME: // This case adds the Element, but requires the user to // enter a unique name. String new_name = prompt.rename(mde_new); if(new_name != null && new_name.length() > 0) { reason = mds_cur.addElement(mde_new, new_name, mde_values); if(reason == null) { mde_cur = mds_cur.getElement(new_name); cont = true; } else { prompt.renameFailed(mde_new, new_name, reason); cont = false; } } else { if(new_name != null) { prompt.renameFailed(mde_new, new_name, "MSMPrompt.Invalid_Name"); } cont = false; } break; case Declarations.REPLACE: // Removes the existing Element then adds the new. mds_cur.removeElement(mde_cur); reason = mds_cur.addElement(mde_new, mde_values); if(reason == null) { cont = true; } else { prompt.removeFailed(mde_cur, reason); cont = false; } break; case Declarations.SKIP: // Does not change the set. cont = true; break; } // Add this action to profile for later reference. if(profiler == null) { ///ystem.err.println("No Profiler"); } //profiler.addAction(mds_new.getFile().getAbsolutePath(), MSMUtils.getFullName(mde_new), option, MSMUtils.getFullName(mde_cur)); } prompt.incrementMerge(); } prompt.endMerge(); return true; } /** This method allows for two metadata elements to be merged. Essentially merging existing elements give the users such options as keeping or replacing attribute elements as they are merged. * @param mde_cur The Element that already exists in the current metadata sets. * @param mde_new A new Element which has the same name as the current one but different data. * @return A boolean that if true indicats the action was completed. If false then an error or user action has prevented the merge. * TODO Implement */ public boolean mergeMDE(MetadataSet mds_cur, Element mde_cur, MetadataSet mds_new, Element mde_new) { for(Node mdn_new = mde_new.getFirstChild(); mdn_new != null; mdn_new = mdn_new.getNextSibling()) { // Only merge the nodes whose name is 'Attribute' if(mdn_new.getNodeName().equals("Attribute")) { Element att_new = (Element) mdn_new; int action = Declarations.NO_ACTION; Element replace_att = null; // replace this att with the new one // Unfortunately some attributes, such as author, can have several occurances, so match each in turn. Element temp[] = MSMUtils.getAttributeNodesNamed(mde_cur, att_new.getAttribute("name")); if (temp==null) { action = Declarations.ADD; } else { // look for an exact match for(int i = 0; temp != null && i < temp.length; i++) { Element att_cur = temp[i]; if(MSMUtils.attributesEqual(att_cur, att_new)) { action = Declarations.SKIP; break; } att_cur = null; } if (action == Declarations.NO_ACTION) { // we didn't find a match, so we have to prompt teh user for what to do Object result = prompt.mDEPrompt(mde_cur, temp, mde_new, att_new); if (result instanceof Integer) { action = ((Integer)result).intValue(); } else { // we have a replace action, and the returned object is the Attribute to replace action = Declarations.REPLACE; replace_att = (Element)result; } } } // now do the required action switch (action) { case Declarations.REPLACE: // Out with the old. mde_cur.removeChild(replace_att); case Declarations.ADD: // Simply add the new attribute. No clash is possible as we have already tested for it. MSMUtils.add(mde_cur, att_new); break; case Declarations.SKIP: // Do nothing. Move on to next attribute. break; case Declarations.CANCEL: return false; default: } att_new = null; temp = null; replace_att = null; } // if node nmae = attribute } //for each child node return mergeMDV(mds_cur, mde_cur, mds_new, mde_new); } /** Merge two metadata value trees. * @param mds_cur The current MetadataSet. * @param mde_cur The current Element which acts as a value tree root. * @param mds_new The MetadataSet we are merging in. * @param mde_new The Element which acts as a value tree that we are merging in. */ public boolean mergeMDV(MetadataSet mds_cur, Element mde_cur, MetadataSet mds_new, Element mde_new) { // Remember we may be asked to merge with a current mdv of null. return MSMUtils.updateValueTree(mds_cur, mde_cur, mds_new, mde_new); } 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()); // Fire event. fireElementChanged(new MSMEvent(this, null, old_name)); } else { ///ystem.err.println("no such set " + namespace); } // No such set. No such element. } /** Remove a piece of metadata from a record or records[] and fire all the relevant events. * @param records A FileNode[] of records to be changed. * @param metadata The Metadata to remove. */ public void removeMetadata(long id, Metadata metadata, FileNode records[]) { // Reset undo buffer undo_buffer.clear(); // Simplier than the others. Simply remove the metadata. Note that we only bother prompting if there is more than one int action = MetaEditPrompt.CONFIRM; if(records.length == 1) { action = MetaEditPrompt.REMOVE; } // Now remove metadata from the selected file nodes. for(int i = 0; action != MetaEditPrompt.CANCEL && i < records.length; i++) { action = removeMetadata(id, records[i], metadata, action, (records.length > 1)); } // If we were cancelled we should undo any changes so far if(action == MetaEditPrompt.CANCEL) { for(Iterator keys = undo_buffer.keySet().iterator(); keys.hasNext(); ) { FileNode record = (FileNode) keys.next(); undoRemove(id, record); } } } /** Remove the specified listener from ourselves. * @param listener The MSMListener in question. */ public void removeMSMListener(MSMListener listener) { listeners.remove(listener); } public void removeSet(MetadataSet set) { mds_hashtable.remove(set.getNamespace()); fireSetChanged(set); } /** Rename the identifier of a given element to the name given. * @param element The metadata element effected, as an ElementWrapper. * @param new_name The String to use as the new name. */ /* private void renameElement(ElementWrapper element, String new_name) { Element e = element.getElement(); String old_name = element.toString(); MSMUtils.setIdentifier(e, new_name); fireElementChanged(new MSMEvent(this, element, old_name)); old_name = null; e = null; } */ /** A method to save the state of this metadata set manager. First we ensure that the names of all included metadata sets have been added to the collection configuration file, then all of the metadata sets contained are exported with full content to the collect//metadata/ directory. */ public void save() { // Create the correct file to save these sets to... File file = new File(Gatherer.self.getCollectionMetadata()); if (!file.exists()) { Gatherer.println("trying to save a collection and the metadata directory does not exist - creating it!"); file.mkdir(); } // And make back ups of all existing metadata set files. File temp[] = file.listFiles(); for(int i = temp.length - 1; i >= 0; i--) { if(temp[i].getName().endsWith(".mds") || temp[i].getName().endsWith(".mdv")) { File backup = new File(temp[i].getAbsolutePath() + "~"); backup.deleteOnExit(); if(!temp[i].renameTo(backup)) { Gatherer.println("Error in MetadataSetManager.save(): FileRenamedException"); } } } // Now save the latest versions of the metadata sets. for(Enumeration keys = mds_hashtable.keys(); keys.hasMoreElements(); ) { String namespace = (String) keys.nextElement(); MetadataSet set = (MetadataSet) mds_hashtable.get(namespace); try { File mds_file = new File(file, set.getNamespace() + ".mds"); Utility.export(set.getDocument(), mds_file); set.setFile(mds_file); // Now for each element attempt to save its value tree. NodeList elements = set.getElements(); for(int i = elements.getLength() - 1; i >= 0; i--) { ElementWrapper value_element = new ElementWrapper((Element)elements.item(i)); if(value_element.hasValueTree()) { GValueModel value_tree = set.getValueTree(value_element); if(value_tree != null) { File value_file = new File(file, value_element.getName() + ".mdv"); ///ystem.err.println("Saving value file: " + value_file.toString()); Utility.export(value_tree.getDocument(), value_file); // If this is a hierarchy element, write hierarchy file. if(value_element.getNamespace().equals(MetadataSetManager.HIDDEN) || value_tree.isHierarchy()) { write(value_element, value_tree, Gatherer.c_man.getCollectionEtc()); } } } } } catch (Exception error) { error.printStackTrace(); } } profiler.save(); } /** Given a FileNode of the original file and the new FileNode, search for any metadata, either from a greenstone directory archive xml file, or from one of the registered 'plugin' parsers. * @param source The source FileNode. * @param destination The new FileNode. */ public final boolean searchForMetadata(FileNode destination, FileNode source, boolean folder_level) { return searchForMetadata(destination, source, folder_level, false); } public final boolean searchForMetadata(FileNode destination, FileNode source, boolean folder_level, boolean dummy_run) { ///atherer.println("MetadataSetManager.searchForMetadata()"); // for some reason, the loader returns 'dialog_cancelled' ie true if cancelled, false if OK. why??? // since we want to return true if successful, we'll return the opposite return (loader.searchForMetadata(destination, source, folder_level, dummy_run)?false:true); } public final int size() { return getSets().size(); } /** Update a piece of metadata connected to a record or records, ensuring the value tree is built properly, and correct messaging fired. * @param id * @param old_metadata The metadata element, * @param records A FileNode[] of records, or directories, to add the specified metadata to. * @param value_str The value to assign to the metadata as a String. * @param action The default action to take in the prompt. * @param file_level If true then the metadata can be replaced normally, if false then we should actually use an add method instead. * @return The Metadata we just assigned. */ public Metadata updateMetadata(long id, Metadata old_metadata, FileNode records[], String value_str, int action, boolean file_level) { // Retrieve the new value node from the same value tree as the old metadata. ElementWrapper element = old_metadata.getElement(); GValueModel model = getValueTree(element); GValueNode value = null; if(model != null) { value = model.addValue(value_str); } else { value = new GValueNode(element.toString(), value_str); } // Create new metadata. Metadata new_metadata = new Metadata(value); // Reset the undo buffer undo_buffer.clear(); // And update the old with it. if(action == -1) { //Note that we only bother prompting if there is more than one action = MetaEditPrompt.CONFIRM; if(records.length == 1) { action = MetaEditPrompt.OVERWRITE; } } // And then update each selection file node. for(int i = 0; action != MetaEditPrompt.CANCEL && i < records.length; i++) { action = updateMetadata(id, records[i], old_metadata, new_metadata, action, (records.length > 1), file_level); } // If we were cancelled we should undo any changes so far if(action == MetaEditPrompt.CANCEL) { for(Iterator keys = undo_buffer.keySet().iterator(); keys.hasNext(); ) { FileNode record = (FileNode) keys.next(); undoUpdate(id, record); } } // All done. Any events would have been fired within the record recursion. return new_metadata; } /** Add a reference to a piece of metadata to the given FileNode. The whole method gets a wee bit messy as we have to allow for several different commands from users such as accumulate / overwrite, skip just this file or cancel the whole batch. Cancelling is especially problematic as we need to rollback any changes (within reason). * It is also worth mentioning that despite its name, no actual metadata is added directly by this method. Instead a call to fireMetadataChanged() is issued, which is in turn processed by the MetadataXMLFileManager (which, given this method may have been called from MetadataXMLFileManager as well, means the cycle is complete. Um, that doesn't mean theres an infinite loop... I hope). * @param id a long unique identifier shared by all actions caused by the same gesture. * @param record the FileNode we are adding the metadata to. * @param data the new Metadata. * @param action the default action as an int. May require user interaction. * @param multiple_selection true if more than one file or folder was selected. * @return an int specifying the current action. Thus changes in lower parts of the tree continue to effect other disjoint subtrees. */ private int addMetadata(long id, FileNode record, Metadata data, int action, boolean multiple_selection) { // Super special exception for accumulate all action. We are going to add this metadata anyway, regardless of whats already there, so just add it. if(action == MetaEditPrompt.ACCUMULATE_ALL || action == MetaEditPrompt.OVERWRITE_ALL) { fireMetadataChanged(id, record, null, data, action - 1); // Transform action to accumulate or overwrite } else { // Recover the metadata from this file. ArrayList metadata = Gatherer.c_man.getCollection().gdm.getMetadata(record.getFile()); // Most important test, we don't have to add the metadata if its already there! if(!metadata.contains(data)) { // Record undo information for this file node. ArrayList undo = new ArrayList(); // Prepare for MEP int user_action = MetaEditPrompt.ACCUMULATE; String values = ""; // See if there is any existing metadata with the same name. If so make a string from all the values (bob, jim etc). int metadata_size = metadata.size(); for(int i = 0; i < metadata_size; i++) { Metadata current_data = (Metadata)metadata.get(i); if(current_data.getElement().equals(data.getElement())) { if(values.length() > 0) { values = values + ", "; } values = values + current_data.getValue(); } } // If we are confirming prompt for user_action. if(values.length() > 0 && action == MetaEditPrompt.CONFIRM) { MetaEditPrompt mep = new MetaEditPrompt(MetaEditPrompt.ADD_PROMPT, multiple_selection, record.getFile(), data.getElement().toString(), values, data.getValue()); user_action = mep.display(); } if(user_action == MetaEditPrompt.ACCUMULATE_ALL || user_action == MetaEditPrompt.CANCEL || user_action == MetaEditPrompt.OVERWRITE_ALL) { action = user_action; } // If we are overwriting we first remove all metadata with the same element, unless the metadata is non-file level is which case we leave it, and hope the accumulate vs overwrite will be followed during the determining of metadata assigned. if(action == MetaEditPrompt.OVERWRITE_ALL || user_action == MetaEditPrompt.OVERWRITE) { for(int i = metadata_size; i != 0; i--) { Metadata old_data = (Metadata)metadata.get(i - 1); if(old_data.getElement().equals(data.getElement()) && old_data.isFileLevel()) { // We have a match. Remove this metadata. fireMetadataChanged(id, record, old_data, null, MetaEditPrompt.REMOVE); // Add it to our undo buffer. undo.add(old_data); } } } // Ensure the metadata will accumulate or overwrite as the user wishes. if(user_action == MetaEditPrompt.ACCUMULATE || user_action == MetaEditPrompt.ACCUMULATE_ALL) { data.setAccumulate(true); } else if(user_action == MetaEditPrompt.OVERWRITE || user_action == MetaEditPrompt.OVERWRITE_ALL) { data.setAccumulate(false); } // Unless cancelled, add the metadata after checking we don't already have it in the metadata (obviously not if we're overwriting but we'd better check anyway). Also if we've skipped the file we should do so, but move on to the next child... if((user_action == MetaEditPrompt.ACCUMULATE || user_action == MetaEditPrompt.ACCUMULATE_ALL || user_action == MetaEditPrompt.OVERWRITE || user_action == MetaEditPrompt.OVERWRITE_ALL) && !metadata.contains(data)) { ///ystem.err.println("Adding metadata " + data); fireMetadataChanged(id, record, null, data, ((user_action == MetaEditPrompt.ACCUMULATE || user_action == MetaEditPrompt.ACCUMULATE_ALL) ? MetaEditPrompt.ACCUMULATE : MetaEditPrompt.OVERWRITE)); // The last element in undo is the new element. undo.add(data); } // Store the undo list in our undo buffer. undo_buffer.put(record, undo); } } // If we've been cancelled, roll back the addition. if(action == MetaEditPrompt.CANCEL) { undoAdd(id, record); } return action; } /** addMetadata(long, FileNode, Metadata, int, boolean) */ /** Create the hidden mds, used for custom classifiers. */ private MetadataSet createHidden() { MetadataSet hidden_mds = new MetadataSet(Utility.METADATA_SET_TEMPLATE); hidden_mds.setAttribute("creator","The Gatherer"); hidden_mds.setAttribute("contact","gatherer@greenstone"); hidden_mds.setAttribute("description","A hidden metadata set used to create custom classifiers."); hidden_mds.setAttribute("family","Gatherer Hidden Metadata"); hidden_mds.setAttribute("lastchanged",""); hidden_mds.setAttribute("name","Gatherer Hidden Metadata"); hidden_mds.setAttribute("namespace",HIDDEN); mds_hashtable.put(HIDDEN, hidden_mds); fireSetChanged(hidden_mds); return hidden_mds; } /** Creates a new profiler, which in turn will attempt to load previous profile information. */ private void loadProfiler() { profiler = new MSMProfiler(); addMSMListener(profiler); } /** In order to remove metadata from the tree you first call this method providing it with the metadata you want removed. This will remove any occurance of said metadata from the given FileNode (using fireMetadataChanged()). * @param id a unique long identifier common to all actions caused by a single gesture. * @param record the FileNode who we are removing metadata from. * @param data the Metadata you wish removed from the tree. * @param action an int specifying the wanted prompting action. * @param multiple_selection the number of records in the selection, as an int. Used to determine prompt controls. * @return an int specifying the current action. Thus changes in lower parts of the tree continue to effect other disjoint subtrees. */ private int removeMetadata(long id, FileNode record, Metadata data, int action, boolean multiple_selection) { ArrayList metadata = Gatherer.c_man.getCollection().gdm.getMetadata(record.getFile()); int user_action = MetaEditPrompt.REMOVE; // See if we even have this metadata. if(metadata.contains(data)) { ArrayList undo = new ArrayList(); // We do have it. If action == CONFIRM, show user prompt. if(action == MetaEditPrompt.CONFIRM) { MetaEditPrompt mep = new MetaEditPrompt(MetaEditPrompt.REMOVE_PROMPT, multiple_selection, record.getFile(), data.getElement().toString(), data.getValue(), ""); user_action = mep.display(); } // Set action to match the user_action under certain circumstances. if(user_action == MetaEditPrompt.CANCEL || user_action == MetaEditPrompt.REMOVE_ALL) { action = user_action; } if(action == MetaEditPrompt.REMOVE_ALL || user_action == MetaEditPrompt.REMOVE) { fireMetadataChanged(id, record, data, null, MetaEditPrompt.REMOVE); undo.add(data); } // Store undo information undo_buffer.put(record, undo); } // If we've been cancelled higher up, undo action. if(action == MetaEditPrompt.CANCEL) { undoRemove(id, record); } return action; } /** Rollback any changes made as part of a single metadata add process (only valid during the action itself, ie if a user presses cancel). * @param id the unique identify of all actions created as part of a single gesture, as a long. * @param record the FileNode whose metadata was changed. */ private void undoAdd(long id, FileNode record) { // Retrieve the undo data from the buffer ArrayList undo = (ArrayList) undo_buffer.get(record); // If there is no undo then we can't do anything, but there should be if(undo != null && undo.size() > 0) { // The last piece of data in an add actions undo buffer is the metadata that was added Metadata data = (Metadata) undo.remove(undo.size() - 1); // Remove the data fireMetadataChanged(id, record, data, null, MetaEditPrompt.REMOVE); // If we removed other metadata when adding this metadata restore it too for(int i = 0; i < undo.size(); i++) { Metadata old_data = (Metadata) undo.get(i); fireMetadataChanged(id, record, null, old_data, MetaEditPrompt.ACCUMULATE); } } } /** Rollback any changes made as part of a single metadata remove process (only valid during the action itself, ie if a user presses cancel). * @param id the unique identify of all actions created as part of a single gesture, as a long. * @param record the FileNode metadata was removed from. */ private void undoRemove(long id, FileNode record) { // Retrieve undo information ArrayList undo = (ArrayList) undo_buffer.get(record); // Ensure that we have something to undo if(undo != null && undo.size() == 1) { // The undo buffer should contain exactly one entry, the metadata removed Metadata data = (Metadata) undo.get(0); fireMetadataChanged(id, record, null, data, MetaEditPrompt.ACCUMULATE); } } /** Roll back any changes made as part of a single metadata update process (only valid during the action itself, ie if a user presses cancel). * @param id the unique identify of all actions created as part of a single gesture, as a long. * @param record the FileNode whose metadata was changed. */ private void undoUpdate(long id, FileNode record) { // Retrieve undo information ArrayList undo = (ArrayList) undo_buffer.get(record); if(undo != null && undo.size() == 2) { Metadata old_data = (Metadata) undo.get(0); Metadata new_data = (Metadata) undo.get(1); fireMetadataChanged(id, record, new_data, null, MetaEditPrompt.REMOVE); if(old_data != new_data) { // Correct reference comparison fireMetadataChanged(id, record, null, old_data, MetaEditPrompt.ACCUMULATE); } } } /** Used to update the values of one of the metadata elements within this node. Has the same trickiness as Add but only half the number of options. * @param id the unique identify of all actions created as part of a single gesture, as a long. * @param record the FileNode whose metadata we are changing. * @param old_data The old existing Metadata. * @param new_data The new updated Metadata. * @param action An int indicating what we are going to do about it. * @param multiple_selection The number of records in the selection, as an int. Used to determine prompt controls. * @return An int specifying the current action. Thus changes in lower parts of the tree continue to effect other disjoint subtrees. */ private int updateMetadata(long id, FileNode record, Metadata old_data, Metadata new_data, int action, boolean multiple_selection, boolean file_level) { ArrayList metadata; if(file_level) { metadata = Gatherer.c_man.getCollection().gdm.getMetadata(record.getFile()); } else { metadata = Gatherer.c_man.getCollection().gdm.getAllMetadata(record.getFile()); } int user_action = MetaEditPrompt.OVERWRITE; // Standard case of updating an existing metadata value. if(metadata.contains(old_data)) { ArrayList undo = new ArrayList(); // If we are to prompt the user, do so. if(action == MetaEditPrompt.CONFIRM) { MetaEditPrompt mep = new MetaEditPrompt(MetaEditPrompt.UPDATE_PROMPT, multiple_selection, record.getFile(), old_data.getElement().toString(), old_data.getValue(), new_data.getValue()); user_action = mep.display(); } // Some user actions should have a continuous effect. if(user_action == MetaEditPrompt.OVERWRITE_ALL || user_action == MetaEditPrompt.CANCEL) { action = user_action; } // And if the update chose update, do so. if(action == MetaEditPrompt.OVERWRITE_ALL || user_action == MetaEditPrompt.OVERWRITE || user_action == MetaEditPrompt.UPDATE_ONCE) { ///ystem.err.println("Updating:\n"+old_data+"\nto\n"+new_data); // If this is file level then we can do a normal replace if(file_level) { fireMetadataChanged(id, record, old_data, new_data, MetaEditPrompt.OVERWRITE); undo.add(old_data); undo.add(new_data); } // Otherwise we are dealing with someone attempting to override inherited metadata, so we actually fire an add. To this end we add new data twice to the undo buffer, thus we can detect if this has happened. else { fireMetadataChanged(id, record, null, new_data, MetaEditPrompt.OVERWRITE); undo.add(new_data); undo.add(new_data); } } // Store the undo information undo_buffer.put(record, undo); } // If we've been cancelled undo. if(action == MetaEditPrompt.CANCEL) { undoUpdate(id, record); } return action; } private void write(ElementWrapper element, GValueModel model, String etc_dir) { try { File out_file = new File(etc_dir + element.getName() + ".txt"); FileOutputStream fos = new FileOutputStream(out_file); OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8"); BufferedWriter bw = new BufferedWriter(osw, Utility.BUFFER_SIZE); Vector all_values = model.traverseTree(); for(int i = 0; i < all_values.size(); i++) { GValueNode node = (GValueNode)all_values.get(i); TreePath path = new TreePath(node.getPath()); String full_value = node.getFullPath(false); String index = model.getHIndex(full_value); write(bw, "\"" + full_value + "\"\t" + index + "\t\"" + node.toString(GValueNode.GREENSTONE) + "\""); } // Very important we do this, or else buffer may not be flushed bw.flush(); bw.close(); } catch(Exception error) { error.printStackTrace(); } } private void write(Writer w, String text) throws Exception { text = text + "\r\n"; char buffer[] = text.toCharArray(); w.write(buffer, 0, buffer.length); } }