tufts.vue.action.ActionUtil.java Source code

Java tutorial

Introduction

Here is the source code for tufts.vue.action.ActionUtil.java

Source

/*
* Copyright 2003-2010 Tufts University  Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 * 
 * http://www.osedu.org/licenses/ECL-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package tufts.vue.action;

import tufts.Util;
import tufts.vue.VueUtil;
import tufts.vue.VUE;
import tufts.vue.UrlAuthentication;
import tufts.vue.LWMap;
import tufts.vue.VueFileFilter;
import tufts.vue.VueResources;
import tufts.vue.XMLUnmarshalListener;
import tufts.vue.DEBUG;
import tufts.vue.gui.VueFileChooser;

import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.MarshalListener;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.xml.UnmarshalListener;
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.ValidationException;

import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.mapping.MappingException;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import edu.tufts.vue.preferences.PreferencesManager;

import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.ListCellRenderer;
import javax.swing.plaf.FileChooserUI;
import javax.swing.plaf.basic.BasicFileChooserUI;

import java.net.URL;
import java.net.URI;
import java.util.HashMap;
import java.util.Locale;
import java.awt.Component;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.net.*;

/**
 * A class which defines utility methods for any of the action class.
 * Most of this code is for save/restore persistence thru castor XML.
 *
 * @version $Revision: 1.142 $ / $Date: 2010-02-03 19:13:45 $ / $Author: mike $
 * @author  Daisuke Fujiwara
 * @author  Scott Fraize
 */

public class ActionUtil {
    private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(ActionUtil.class);

    private final static String XML_MAPPING_CURRENT_VERSION_ID = VueResources
            .getString("mapping.lw.current_version");
    private final static URL XML_MAPPING_DEFAULT = VueResources
            .getURL("mapping.lw.version_" + XML_MAPPING_CURRENT_VERSION_ID);
    private final static URL XML_MAPPING_UNVERSIONED = VueResources.getURL("mapping.lw.version_none");
    private final static URL XML_MAPPING_OLD_RESOURCES = VueResources.getURL("mapping.lw.version_resource_fix");

    private final static String VUE_COMMENT_START = "<!-- Do Not Remove:";
    private final static String OUTPUT_ENCODING = "US-ASCII";
    private final static String DEFAULT_WINDOWS_ENCODING = "windows-1252"; // (a.k.a Cp1252) for reading pre ASCII enforced save files from Windows
    private final static String DEFAULT_MAC_ENCODING = "UTF-8"; // "MacRoman" not supported on Windows platform

    /**
     * Our default is to always read with an input encoding of UTF-8, even if the XML
     * was written with a US-ASCII encoding.  This is because pure ascii will translate
     * fine through UTF-8, but in case it winds up being that the XML was written out my
     * the marshaller with a UTF-8 encoding, we're covered.  (tho maybe with extremely
     * save files with platform specific encodings, (e.g, MacRoman or
     * windows-1255/Cp1255) we'll lose a special char here or there, such as left-quote
     * and right-quote).
     */
    private final static String DEFAULT_INPUT_ENCODING = "UTF-8"; // safest default input encoding

    // Note: the encoding format of the incoming file will normally either be UTF-8 for
    // older VUE save files, or US-ASCII for newer files.  In any case, the encoding is
    // indicated in the <?xml> tag at the top of the file, and castor handles adjusting
    // for it.  If we want to write UTF-8 files, we have to be sure the stream that's
    // created to write the file is created with the same encoding, or we sometimes get
    // problems, depending on the platform.  We always READ (unmarshall) via a UTF-8
    // stream, no matter what, as US-ASCII will pass through a UTF-8 stream untouched,
    // and it will handle UTF-8 if that turns out to be the encoding.

    public ActionUtil() {
    }

    /**A static method which displays a file chooser for the user to choose which file to save into.
       It returns the selected file or null if the process didn't complete*/
    protected static VueFileChooser saveChooser = null;

    public static File selectFile(String title, final String fileType) {
        File picked = null;

        saveChooser = VueFileChooser.getVueFileChooser();

        saveChooser.setDialogTitle(title);
        saveChooser.setAcceptAllFileFilterUsed(false);
        //chooser.set

        if (fileType != null && !fileType.equals("export"))
            saveChooser.setFileFilter(new VueFileFilter(fileType));
        else if (fileType != null && fileType.equals("export")) {
            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.JPEG_DESCRIPTION));
            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.PNG_DESCRIPTION));
            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.SVG_DESCRIPTION));
            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.IMS_DESCRIPTION));
            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.IMAGEMAP_DESCRIPTION));
            //  chooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.ZIP_DESCRIPTION));
        } else {
            VueFileFilter defaultFilter = new VueFileFilter(VueFileFilter.VUE_DESCRIPTION);

            saveChooser.addChoosableFileFilter(defaultFilter);
            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.VPK_DESCRIPTION));
            //SIMILE    chooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.SIMILE_DESCRIPTION));
            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.IMAGEMAP_DESCRIPTION));
            saveChooser.addChoosableFileFilter(new VueFileFilter("PDF"));

            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.JPEG_DESCRIPTION));
            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.PNG_DESCRIPTION));
            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.SVG_DESCRIPTION));
            //chooser.addChoosableFileFilter(new VueFileFilter("html"));

            saveChooser.addChoosableFileFilter(new VueFileFilter("RDF"));
            saveChooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.IMS_DESCRIPTION));
            //   chooser.addChoosableFileFilter(new VueFileFilter(VueFileFilter.ZIP_DESCRIPTION));

            //chooser.addChoosableFileFilter(new VueFileFilter("HTML Outline", "htm"));

            saveChooser.setFileFilter(defaultFilter);
        }
        saveChooser.addPropertyChangeListener(new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent arg0) {
                if (arg0.getPropertyName() == VueFileChooser.FILE_FILTER_CHANGED_PROPERTY) {
                    adjustExtension();
                }
            }
        });
        adjustExtension();
        int option = saveChooser.showSaveDialog(VUE.getDialogParentAsFrame());//, VueResources.getString("dialog.save.title"));

        if (option == VueFileChooser.APPROVE_OPTION) {
            picked = saveChooser.getSelectedFile();

            String fileName = picked.getAbsolutePath();

            /**
             * 2009-10-16 There was a bug in here that sliced up the filename, I think removing this block
             * of code should fix the issue. If you had a name like Object.Generic-Tufts, VUE would get
             * all confused and put files in the wrong directory or make a file called Object the filename
             * slicing seemed unnecessary. -MK
             * 
             */
            String extension = ((VueFileFilter) saveChooser.getFileFilter()).getExtensions()[0];
            //if it isn't a file name with the right extension 
            if (!picked.getName().endsWith("." + extension)) {
                fileName += "." + extension;
                picked = new File(fileName);
            }

            if (picked.exists()) {
                int n = VueUtil.confirm(null,
                        VueResources.getString("replaceFile.text") + " \'" + picked.getName() + "\'",
                        VueResources.getString("replaceFile.title"), JOptionPane.YES_NO_OPTION,
                        JOptionPane.WARNING_MESSAGE);

                if (n == JOptionPane.NO_OPTION) {
                    picked = null;
                    saveChooser.showDialog(VUE.getDialogParentAsFrame(),
                            VueResources.getString("dialog.save.title"));
                }
            }

            if (picked != null)
                VueUtil.setCurrentDirectoryPath(picked.getParent());
        }

        return picked;
    }

    private final static void adjustExtension() {
        BasicFileChooserUI ui = (BasicFileChooserUI) saveChooser.getUI();
        String name = ui.getFileName();

        String baseName = null;
        String extension = ((VueFileFilter) saveChooser.getFileFilter()).getExtensions()[0];

        if (name == null || (name != null && name.length() < 1))
            baseName = VUE.getActiveMap().getLabel();
        else
            baseName = name;

        if (name == null || (name != null && name.length() < 1)) {
            if (baseName.lastIndexOf(".") > 0)
                baseName = VUE.getActiveMap().getLabel().substring(0, baseName.lastIndexOf("."));
        } else {
            if (baseName.lastIndexOf(".") > 0)
                baseName = baseName.substring(0, baseName.lastIndexOf("."));

        }
        String curDir = saveChooser.getCurrentDirectory().getAbsolutePath();
        File f = new File(curDir + File.separator + baseName + "." + extension);
        if (f.exists())
            baseName = baseName + "-copy" + "." + extension;
        else
            baseName = baseName + "." + extension;

        ActionUtil.saveChooser.setSelectedFile(new File(baseName));

    }

    /**A static method which displays a file chooser for the user to choose which file to open.
       It returns the selected file or null if the process didn't complete
    TODO BUG: do not allow more than one dialog open at a time -- two "Ctrl-O" in quick succession
    will open two open file dialogs. */

    public static File openFile(String title, String extension) {
        File file = null;

        VueFileChooser chooser = VueFileChooser.getVueFileChooser();

        int option = chooser.showOpenDialog(VUE.getDialogParent());

        if (option == VueFileChooser.APPROVE_OPTION) {
            final File chooserFile = chooser.getSelectedFile();
            if (chooserFile == null)
                return null;
            final String fileName;
            final String chosenPath = chooserFile.getAbsolutePath();

            // if they type a file name w/out an extension
            if (chooserFile.getName().lastIndexOf('.') < 0)
                fileName = chosenPath + "." + extension;
            else
                fileName = chosenPath;

            file = new File(fileName);

            if (file.exists()) {
                VueUtil.setCurrentDirectoryPath(chooser.getSelectedFile().getParent());
            } else {
                File dir = new File(chosenPath);
                if (dir.exists() && dir.isDirectory()) {
                    //System.out.println("chdir " + chosenPath);
                    VueUtil.setCurrentDirectoryPath(chosenPath);
                } else {
                    Log.debug("File '" + chosenPath + "' " + file + " can't  be found.");
                    tufts.vue.VueUtil.alert(chooser,
                            VueResources.getString("actionutil.filenotfound.error") + " " + file,
                            VueResources.getString("actionutil.filenotfound.error"));
                }
                file = null;
            }
        }
        return file;
    }

    /**A static method which displays a file chooser for the user to choose which file to open.
    It returns the selected file or null if the process didn't complete
    TODO BUG: do not allow more than one dialog open at a time -- two "Ctrl-O" in quick succession
    will open two open file dialogs. */

    public static File[] openMultipleFiles(String title, String extension) {
        File file = null;

        VueFileChooser chooser = VueFileChooser.getVueFileChooser();

        chooser.setDialogTitle(title);
        chooser.setMultiSelectionEnabled(true);
        chooser.setFileFilter(new VueFileFilter(extension));

        int option = chooser.showOpenDialog(VUE.getDialogParent());

        if (option == VueFileChooser.APPROVE_OPTION) {
            final File[] chooserFile = chooser.getSelectedFiles();
            if (chooserFile == null)
                return null;
            final String fileName;

            if (chooserFile.length == 1) {
                //this scenario can only happen if there's only 1 file in the array...

                final String chosenPath = chooserFile[0].getAbsolutePath();

                // if they type a file name w/out an extension
                if (chooserFile[0].getName().lastIndexOf('.') < 0)
                    fileName = chosenPath + "." + extension;
                else
                    fileName = chosenPath;

                chooserFile[0] = new File(fileName);

                if (chooserFile[0].exists()) {
                    VueUtil.setCurrentDirectoryPath(chooser.getSelectedFile().getParent());
                } else {
                    File dir = new File(chosenPath);
                    if (dir.exists() && dir.isDirectory()) {
                        //System.out.println("chdir " + chosenPath);
                        VueUtil.setCurrentDirectoryPath(chosenPath);
                    } else {
                        Log.debug("File '" + chosenPath + "' " + file + " can't  be found.");
                        tufts.vue.VueUtil.alert(chooser,
                                VueResources.getString("actionutil.filenotfound.error") + " " + file,
                                VueResources.getString("actionutil.filenotfound.error"));
                    }
                    chooserFile[0] = null;
                }
            }
            return chooserFile;
        }
        return null;
    }

    /**
     * Return the current mapping used for saving new VUE data.
     */
    public static Mapping getDefaultMapping() {
        Object result = _loadMapping(XML_MAPPING_DEFAULT);
        if (result instanceof Exception) {
            VueUtil.alert(null, VueResources.getString("dialog.mappingfile.message") + "\n"
                    + VueResources.getString("dialog.mappingurl.message") + XML_MAPPING_DEFAULT + "\n" + result,
                    VueResources.getString("dialog.mappingfile.title"), JOptionPane.ERROR_MESSAGE);
        }
        return (Mapping) result;
    }

    public static Unmarshaller getDefaultUnmarshaller() throws org.exolab.castor.mapping.MappingException {
        return getDefaultUnmarshaller(null, "(unknown source)");
    }

    public static Unmarshaller getDefaultUnmarshaller(String sourceName)
            throws org.exolab.castor.mapping.MappingException {
        return getDefaultUnmarshaller(null, sourceName);
    }

    /**
     * Return the default unmarshaller for VUE data, which includes an installed
     * unmarshall listener, which is required for the proper restoration of VUE objects.
     */
    public static Unmarshaller getDefaultUnmarshaller(Mapping mapping, String sourceName)
            throws org.exolab.castor.mapping.MappingException {
        if (mapping == null)
            mapping = getDefaultMapping();

        // todo: can cache this with it's mapping set (tho need to cache by mapping,
        // as we still have different mapping files for old versions of the VUE save file)
        Unmarshaller unmarshaller = new Unmarshaller();

        unmarshaller.setIgnoreExtraAttributes(true);
        unmarshaller.setIgnoreExtraElements(true);
        unmarshaller.setValidation(false);
        unmarshaller.setObjectFactory(new XMLObjectFactory(sourceName));
        //unmarshaller.setWhitespacePreserve(true); // doesn't affect elements!  (e.g. <notes> foo bar </notes>)
        // HOWEVER: castor 0.9.7 now automatically encodes/decodes white space for attributes...
        unmarshaller.setLogWriter(new PrintWriter(System.err)); // todo: deprecated; now uses commons-logging

        if (DEBUG.XML)
            unmarshaller.setDebug(true);

        unmarshaller.setUnmarshalListener(new MapUnmarshalHandler(sourceName, "DEFAULT(" + sourceName + ")"));
        unmarshaller.setMapping(mapping);

        if (DEBUG.CASTOR || DEBUG.XML || DEBUG.IO)
            Log.debug("got default unmarshaller for mapping " + mapping + " source " + sourceName);

        return unmarshaller;
    }

    private static Mapping getMapping(URL mappingSource) {
        if (DEBUG.IO)
            Log.debug("Fetching mapping: " + mappingSource);
        Object result = _loadMapping(mappingSource);
        if (result instanceof Mapping)
            return (Mapping) result;
        else
            return null;
    }

    private static HashMap LoadedMappings = new HashMap();

    /** return's a Mapping if successful, or an Exception if not.
     * Results are cached (if load was successful) for future calls.*/
    private static Object _loadMapping(URL mappingSource)
    //throws java.io.IOException //, org.exolab.castor.mapping.MappingException
    {
        if (!DEBUG.CASTOR && LoadedMappings.containsKey(mappingSource))
            return (Mapping) LoadedMappings.get(mappingSource);
        Mapping mapping = new Mapping();
        if (DEBUG.IO || DEBUG.INIT)
            Log.debug("Loading mapping " + mappingSource + "...");
        try {
            mapping.loadMapping(mappingSource);
        } catch (Exception e) { // MappingException or IOException
            e.printStackTrace();
            System.err.println("Failed to load mapping " + mappingSource);
            return e;
        }
        if (DEBUG.IO || DEBUG.INIT)
            Log.debug("Loaded mapping " + mappingSource);
        LoadedMappings.put(mappingSource, mapping);
        return mapping;
    }

    public static class VueMarshalListener implements MarshalListener {
        public boolean preMarshal(Object o) {
            //if (true||DEBUG.XML) Log.debug("VML  pre: " + Util.tags(o));
            //if (o instanceof tufts.vue.Resource)
            try {
                // TODO: create a ConditionalMarshalling interface for embedding this logic
                // in the client classes so it's not kept here.

                // Note that ALL objects pass through here.

                String key = null;
                if (o.getClass() == tufts.vue.PropertyEntry.class) // is final class
                    key = ((tufts.vue.PropertyEntry) o).getEntryKey();

                if (key != null && tufts.vue.Resource.isRuntimePropertyKey(key)) {
                    if (DEBUG.XML)
                        Log.debug(" no-marshal " + Util.tags(o));
                    return false;
                } else {
                    if (DEBUG.XML)
                        Log.debug("marshalling " + Util.tags(o));
                    return true;
                }
            } catch (Throwable t) {
                Util.printStackTrace(t, "Marshalling condition failure on " + o);
            }
            return true;
        }

        public void postMarshal(Object o) {
            //if (true||DEBUG.XML) Log.debug("VML post: " + Util.tags(o));
        }
    }

    /*
     * This method checks whether a file can be safely saved and opened by castor
     * @param map the map whose save compatiblity needs to be tested
     * @returrn true if compatible to castor save, false otherwise 
     */
    public static boolean isCastorCompatible(LWMap map) {
        try {
            File tempFile = File.createTempFile("vueTest", "vue");
            tempFile.deleteOnExit();
            marshallMap(tempFile, map);
            unmarshallMap(tempFile);
            return true;
        } catch (Throwable t) {
            tufts.vue.VueUtil.alert(VueResources.getString("actionutil.filesave.error") + t,
                    VueResources.getString("actionutil.filesave.title"));
            Log.error("Testing save: " + map + ";" + t);
            Util.printStackTrace(t);
        }
        return false;
    }

    private static class WrappedMarshallException extends RuntimeException {
        WrappedMarshallException(Throwable cause) {
            super(cause);
        }
    }

    /**A static method which creates an appropriate marshaller and marshal the active map*/
    public static void marshallMap(File file) {
        marshallMap(file, tufts.vue.VUE.getActiveMap());
    }

    /**
     * Marshall the given map to XML and write it out to the given file.
     */
    public static void marshallMap(File targetFile, LWMap map) {
        File tmpFile = null;

        try {
            tmpFile = File.createTempFile(targetFile.getName() + "$new", ".vue", targetFile.getParentFile());
            if (DEBUG.IO)
                Log.debug("created new tmp file " + tmpFile);
            doMarshallMap(targetFile, tmpFile, map);
        } catch (Throwable t) {
            if (t instanceof WrappedMarshallException)
                t = t.getCause();
            Log.error("marshalling: " + map + "; to " + tmpFile + "; destination: " + targetFile, t);
            // until everyone has chance to update their code
            // to handle the exceptions, wrap this in a runtime exception.
            throw new RuntimeException(t);
        }
        //if (DEBUG.EnableD) Log.debug("

        if (targetFile.exists()) {
            File backup = null;
            try {
                final String backupName = String.format(".~%s", targetFile.getName());
                //if (DEBUG.IO) Log.debug(String.format("creating backup named [%s]", backupName));
                backup = new File(targetFile.getParent(), backupName);
                if (backup.delete()) // Required on Win32 or rename will fail
                    Log.info("  deleted prior backup: " + backup);
                Log.info("renaming old to backup: " + backup);
                if (!targetFile.renameTo(backup))
                    Log.warn("failed to make backup of " + targetFile);
            } catch (Throwable t) {
                Log.warn("backup failed: " + backup, t);
            }
        }

        Log.info("renaming new to target: " + targetFile);
        //         if (targetFile.delete()) // Required on Win32 or rename will fail
        //             Log.warn("deleted prior save file: " + targetFile);
        if (!tmpFile.renameTo(targetFile)) {
            Log.error("Failed to rename temp file " + tmpFile + "; to target file: " + targetFile);
            //Object obj[] = {tmpFile,targetFile};
            // VueResources.getFormatMessage(obj, pattern)
            VueUtil.alert(String.format(Locale.getDefault(), VueResources.getString("actionutil.rename.error"),
                    tmpFile, targetFile), VueResources.getString("actionutil.rename.title"));

        }
    }

    private static void doMarshallMap(final File targetFile, final File tmpFile, final LWMap map)
            throws java.io.IOException, org.exolab.castor.xml.MarshalException,
            org.exolab.castor.xml.ValidationException, org.exolab.castor.mapping.MappingException {
        final String path = tmpFile.getAbsolutePath().replaceAll("%20", " ");
        final FileOutputStream fos = new FileOutputStream(path);
        final OutputStreamWriter writer;
        FileDescriptor FD = null;

        try {
            FD = fos.getFD();
            // getting the FileDescriptor is not required -- we just use it
            // for a final call to sync after we save to increase the likelyhood
            // of our save file actually making it to disk.
        } catch (Throwable t) {
            Log.warn("No FileDescriptor for " + path + "; failsafe sync will be skipped: " + t);
        }

        if (OUTPUT_ENCODING.equals("UTF-8") || OUTPUT_ENCODING.equals("UTF8")) {
            writer = new OutputStreamWriter(fos, OUTPUT_ENCODING);
        } else {

            // For the actual file writer we can use the default encoding because we're
            // marshalling specifically in US-ASCII.  E.g., because we direct castor to
            // fully encode any special characters via setEncoding("US-ASCII"), we'll
            // only have ASCII chars to write anyway, and any default encoding will
            // handle that...

            writer = new OutputStreamWriter(fos);

            // below creates duplicate FOS, but may be better for debug?  (MarshallException's can find file?)
            //if (FD == null)
            //    writer = new FileWriter(path);
            //else
            //    writer = new FileWriter(FD);

        }

        if (DEBUG.IO) {
            try {
                Log.debug(String.format("%s; %s; encoding: \"%s\", which will represent \"%s\" XML content",
                        tmpFile, writer, writer.getEncoding(), OUTPUT_ENCODING));
            } catch (Throwable t) {
                Log.warn(t);
            }
        }

        //=======================================================
        // Marshall the map to the tmp file:
        // ---------------------------------

        marshallMapToWriter(writer, map, targetFile, tmpFile);

        //=======================================================

        // Run a filesystem sync if we can just to be sure: Especially helpful on some
        // linux file systems, such as Ext3, which may not normally touch the disk for
        // another 5 seconds, or XFS/Ext4, which may take their own sweet time.
        // For more see:
        // https://bugs.launchpad.net/ubuntu/+source/linux/+bug/317781/comments/54

        if (DEBUG.IO)
            Log.debug("flushing " + writer);
        writer.flush();

        if (FD != null) {
            try {
                if (DEBUG.IO)
                    Log.debug("syncing " + FD + "; for " + tmpFile);
                // just as backup -- must however be done before writer.close()
                FD.sync();
                Log.info(" sync'd " + FD + "; for " + tmpFile);
            } catch (Throwable t) {
                Log.warn("after save to " + targetFile + "; sync failed: " + t);
            }
        }

        if (DEBUG.IO)
            Log.debug("closing " + writer);
        writer.close();
        if (DEBUG.IO)
            Log.debug(" closed " + writer);

    }

    /**
     * Marshall the given map to the given Writer without touching the map in any
     * way.
     */
    static void marshallMapToWriter(final LWMap map, final Writer writer)
            throws java.io.IOException, org.exolab.castor.xml.MarshalException,
            org.exolab.castor.xml.ValidationException, org.exolab.castor.mapping.MappingException {
        marshallMapToWriter(writer, map, null, null);
    }

    /**
     * @param file - if null, map state is untouched, otherwise, map state is updated
     */
    private static void marshallMapToWriter(final Writer writer, final LWMap map, final File targetFile,
            final File tmpFile) throws java.io.IOException, org.exolab.castor.xml.MarshalException,
            org.exolab.castor.xml.ValidationException, org.exolab.castor.mapping.MappingException {
        map.makeReadyForSaving(targetFile);

        Log.info("marshalling " + map + " to: " + tmpFile);

        Marshaller marshaller = null;
        String name = "";
        if (targetFile != null)
            name = targetFile.getName();
        else
            name = map.getLabel();
        if (name == null)
            name = "";

        final java.util.Date date = new java.util.Date();
        final String today = new java.text.SimpleDateFormat("yyyy-MM-dd").format(date);

        String headerText = VueResources.getString("vue.version") + " concept-map (" + name + ") " + today;

        headerText = org.apache.commons.lang.StringEscapeUtils.escapeXml(headerText);

        writer.write("<!-- Tufts VUE " + headerText + " -->\n");

        writer.write("<!-- Tufts VUE: http://vue.tufts.edu/ -->\n");

        writer.write(VUE_COMMENT_START + " VUE mapping " + "@version(" + XML_MAPPING_CURRENT_VERSION_ID + ")" + " "
                + XML_MAPPING_DEFAULT + " -->\n");
        writer.write(VUE_COMMENT_START + " Saved date " + date + " by " + VUE.getSystemProperty("user.name")
                + " on platform " + VUE.getSystemProperty("os.name") + " " + VUE.getSystemProperty("os.version")
                + " in JVM " + VUE.getSystemProperty("java.runtime.version") + " -->\n");
        writer.write(VUE_COMMENT_START + " Saving version " + tufts.vue.Version.WhatString + " -->\n");
        if (DEBUG.CASTOR || DEBUG.IO)
            Log.debug("Wrote VUE header to " + writer);
        marshaller = new Marshaller(writer);
        //marshaller.setDebug(DEBUG.CASTOR);
        marshaller.setEncoding(OUTPUT_ENCODING);
        // marshaller.setEncoding("UTF-8");
        // marshal as document (default): make sure we add at top: <?xml version="1.0" encoding="<encoding>"?>
        marshaller.setMarshalAsDocument(true);
        marshaller.setNoNamespaceSchemaLocation("none");
        marshaller.setMarshalListener(new VueMarshalListener());
        // setting to "none" gets rid of all the spurious tags like these:
        // xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        //marshaller.setDoctype("foo", "bar"); // not in 0.9.4.3, must wait till we can run 0.9.5.3+

        /*
          marshaller.setMarshalListener(new MarshalListener() {
          public boolean preMarshal(Object o) {
          System.out.println(" preMarshal " + o.getClass().getName() + " " + o);
          return true;
          }
          public void postMarshal(Object o) {
          System.out.println("postMarshal " + o.getClass().getName() + " " + o);
          }
          });
        */

        //marshaller.setRootElement("FOOBIE"); // overrides name of root element

        marshaller.setMapping(getDefaultMapping());

        //----------------------------------------------------------------------------------------
        // 
        // 2007-10-01 SMF -- turning off validation during marshalling now required
        // w/castor-1.1.2.1-xml.jar, otherwise, for some unknown reason, LWLink's
        // with any connected endpoints cause validation exceptions when attempting to
        // save.  E.g, from a map with one node and one link connected to it:
        //
        // ValidationException: The following exception occured while validating field: childList of class:
        // tufts.vue.LWMap: The object associated with IDREF "LWNode[2         "New Node"  +415,+24 69x22]" of type
        // class tufts.vue.LWNode has no ID!;
        // - location of error: XPATH: /LW-MAP
        // The object associated with IDREF "LWNode[2         "New Node"  +415,+24 69x22]" of type class tufts.vue.LWNode has no ID!
        //
        // Even tho the node's getID() is correctly returning "2"
        //
        marshaller.setValidation(false);
        //----------------------------------------------------------------------------------------

        marshaller.setLogWriter(new PrintWriter(System.err)); // todo: deprecated; now uses commons-logging

        // Make modifications to the map at the last minute, so any prior exceptions leave the map untouched.

        final int oldModelVersion = map.getModelVersion();
        final File oldSaveFile = map.getFile();
        if (targetFile != null) {
            map.setModelVersion(LWMap.getCurrentModelVersion());
            // note that if this file is different from it's last save file, this
            // operation may cause any/all of the resources in the map to be
            // updated before returning.
            map.setFile(targetFile);
        }
        // map.addNode(new tufts.vue.LWNode("Hello "+((char)15)+((char)23)));

        //if (DEBUG.CASTOR || DEBUG.IO) System.out.println("Marshalling " + map + " ...");
        Log.debug("marshalling " + map + " ...");
        writer.flush();
        //map.addNode(new tufts.vue.LWNode("Hello World:"+((char)11)));
        try {

            //-----------------------------------------------------------------------------
            //-----------------------------------------------------------------------------
            // try the test map first
            // TODO: DOES NOT ACTUALLY DO A TEST WRITE FIRST
            // It will still completely blow away the user's map if there is any kind of error.
            // Was this ever tested?
            //-----------------------------------------------------------------------------
            //-----------------------------------------------------------------------------

            marshaller.marshal(map);
            writer.flush();
            if (DEBUG.Enabled)
                Log.debug("marshalled " + map + " to " + writer + "; file=" + tmpFile);
        } catch (Throwable t) {
            Log.error(tmpFile + "; " + map, t);

            //-----------------------------------------------------------------------------
            // This was a poor choice of message.  This describes just one of many,
            // many errors that may occur, and can be entirely misleading.
            // VueUtil.alert("The map contains characters that are not supported. Reverting to earlier saved version",
            //-----------------------------------------------------------------------------

            VueUtil.alert(
                    VueResources.getString("actionutil.filecorrupted.error") + "\n\n"
                            + VueResources.getString("actionutil.filecorrupted.description") + "\n"
                            + Util.formatLines(t.toString(), 80),
                    VueResources.getString("actionutil.rename.title"));

            try {
                if (targetFile != null) {
                    // revert map model version & save file
                    map.setModelVersion(oldModelVersion);
                    map.setFile(oldSaveFile);
                }
            } catch (Throwable tx) {
                VueUtil.alert(Util.formatLines(tx.toString(), 80),
                        VueResources.getString("actionutil.internalsave.title"));
                Util.printStackTrace(tx);
            } finally {
                throw new WrappedMarshallException(t);
            }
        }

        if (tmpFile != null) {
            try {
                // should never fail, but if it does, the save itself has still worked
                map.markAsSaved();
            } catch (Throwable t) {
                Log.error(t);
            }
            try {
                Log.info("wrote " + map + " to " + tmpFile);
            } catch (Throwable t) {
                Log.error("debug", t);
            }
        }
    }

    public static LWMap unmarshallMap(File file) throws IOException {
        return unmarshallMap(file, null);
    }

    /** Unmarshall a LWMap from the given file (XML map data) */
    public static LWMap unmarshallMap(File file, MapUnmarshalHandler handler) throws IOException {
        if (file.isDirectory())
            throw new MapException("is a directory, not a map file: " + file);
        if (!file.exists())
            throw new FileNotFoundException("does not exist");
        if (file.length() == 0)
            throw new EmptyFileException();

        return unmarshallMap(file.toURL(), handler);
    }

    private static class MapReader {
        final BufferedReader reader;
        final File file;

        MapReader(BufferedReader r, File f) {
            reader = r;
            file = f;
        }
    }

    private static MapReader getMapReaderForURL(URL url, String charsetEncoding, boolean allowRedirects)
            throws java.io.IOException {

        // Not sure if it's still worth paying attention to the encoding, but we do use
        // any special encoding (if found) from the map file just in case.  All
        // current-day VUE maps should be in pure US-ASCII, so we could theoretically
        // ignore the encoding, but this may be needed for some very old maps.

        File file = tufts.vue.Resource.getLocalFileIfPresent(url);

        if (file != null && file.isDirectory())
            throw new MapException("cannot open " + file + ": is directory");

        Reader reader = null;

        try {

            if (file != null) {

                if (charsetEncoding != null)
                    reader = new InputStreamReader(new FileInputStream(file), charsetEncoding);
                else
                    reader = new FileReader(file); // could default to UTF-8

            } else {

                // No local file was found: it must have been a remote URL

                if (allowRedirects) {
                    final URL redirectURL = tufts.vue.UrlAuthentication.getRedirectedUrl(url, 10); // number of redirects to follow
                    url = redirectURL;
                    file = new File(url.getFile());
                    // AK: Thanks for leaving the comments. 
                    // we need to open the redirect url
                    // SMF 2008-04-08: Anoop's semantics as of 2008-03-12: we do NOT
                    // open the redirect url for reading, we open the original.  Not
                    // sure if this was intended.

                    // This would allow opening the redirect:
                    // url = redirectURL;
                }

                if (charsetEncoding != null)
                    reader = new InputStreamReader(UrlAuthentication.getAuthenticatedStream(url), charsetEncoding);
                else
                    reader = new InputStreamReader(UrlAuthentication.getAuthenticatedStream(url)); // could default to UTF-8

            }

        } catch (Throwable t) {
            Log.error("Could not get reader for: " + file + "; source url=" + url, t);
        }

        if (reader == null) {
            Log.error("No reader found for " + Util.tags(url));
            throw new MapException("no reader found for: " + url);
        }

        Log.debug("got reader for " + Util.tags(url) + "; encoding=" + charsetEncoding + ": " + reader);

        return new MapReader(new BufferedReader(reader), file);

        // Anoop code as of 2008-03-12:
        //         final InputStream urlStream;
        //          final File file; 

        //         if ("file".equals(url.getProtocol())){
        //             //FIX to deal with # problems in the filename
        // //         System.out.println("URL: "+url);
        //              file = new File(url.toString().substring(5));
        //              urlStream =  new BufferedInputStream(new FileInputStream(file)); // remove file:/ from the begining of the file

        //           //  urlStream = url.openStream();
        //         } else {
        //               redirectedUrl = tufts.vue.UrlAuthentication.getRedirectedUrl(url,10); // number of redirects to follow
        //               file = new File(redirectedUrl.getFile());
        //               urlStream = tufts.vue.UrlAuthentication.getAuthenticatedStream(url);
        //             // urlStream =  url.openStream();

        //         }
        //         final BufferedReader reader = new BufferedReader(new InputStreamReader(urlStream, charsetEncoding));

        //         if ("file".equals(url.getProtocol())) {

        //             File file = new File(url.getPath());
        //              if(url.toString().contains("#"))  {
        //                  file = new File(url.getPath()+"#"+url.getRef()); // special case for dealing with # in filename
        //             }

        //             if (file.isDirectory())
        //                 throw new MapException("is directory");

        //             reader = new BufferedReader(new FileReader(file));
        //         } else {
        //             reader = new BufferedReader(new InputStreamReader(tufts.vue.UrlAuthentication.getAuthenticatedStream(url)));
        //             //reader = new BufferedReader(new InputStreamReader(url.openStream()));
        //         }

    }

    /**
     * Input encoding shouldn't matter for the bootstrap reading of the first few lines
     * of the file (as they should be all US_ASCII), tho using DEFAULT_INPUT_ENCODING,
     * instead of null (which will get us the local platform default), would probably
     * make the most sense.  We're leaving this as the default platform encoding (null) for now
     * only because it's been this way for a while...  SMF 2008-04-08
     */
    private static final String BOOTSTRAP_ENCODING = null;

    public static LWMap unmarshallMap(java.net.URL url) throws IOException {
        return unmarshallMap(url, null);
    }

    public static LWMap unmarshallMap(java.net.URL url, MapUnmarshalHandler handler) throws IOException {
        // We scan for lines at top of file that are comments.  If there are NO comment lines, the
        // file is of one of our original save formats that is not versioned, and that may need
        // special processing for the Resource class to Resource interface change over.  If there
        // are comments, the version instance of the string "@version(##)" will set the version ID
        // to ##, and we'll use the mapping appropriate for that version of the save file.

        if (DEBUG.CASTOR || DEBUG.IO) {
            Log.debug("unmarshallMap: " + Util.tags(url));
            //Util.printStackTrace("UM " + url);
        }

        final BufferedReader reader = getMapReaderForURL(url, BOOTSTRAP_ENCODING, false).reader;

        String firstNonCommentLine;
        String versionID = null;
        boolean savedOnWindowsPlatform = false;
        boolean savedOnMacPlatform = false;
        String guessedEncoding = null;
        Mapping mapping = null;

        // We need to skip past the comments to position the reader at the <?xml line for
        // unmarshalling to start.  Also, we look at these comments to determine version of the
        // mapping to use, as well as if it's a pre VUE 1.5 (August 2006) save file, in which case
        // we must guess an encoding, and re-open the file using an InputStreamReader with the
        // proper encoding.

        String savingVersion = "unknown VUE version";

        for (;;) {
            reader.mark(2048); // a single comment line can't be longer than this...
            String line = reader.readLine();
            if (line == null) {
                Log.error("Unexpected end-of-stream in [" + url + "]");
                throw new java.io.IOException("end of stream in " + url);
            }
            if (DEBUG.CASTOR || DEBUG.IO)
                Log.debug("Scanning[" + line + "]");
            if (line.startsWith("<!--") == false) {
                // we should have juadst hit thie "<?xml ..." line -- done with comments
                firstNonCommentLine = line;
                break;
            }

            if (line.startsWith(VUE_COMMENT_START + " Saved")) {

                // The "saved on platform" comments were never expected to be used functionally
                // (only for debug), so determining if the save file was written on a Windows box
                // it's not 100% reliable: e.g., if a user somehow had the name "platform Windows",
                // we would mistake this "saved by" for a "saved on", but we're just going to take
                // this risk -- this is just a workaround backward compat hack because castor
                // wasn't naming the real encoding in it's XML output (turns out it was always
                // puting UTF-8), even if it was using the default Windows encoding of
                // Cp1252/windows-1252.

                //if (DEBUG.IO) System.out.println("scanning for Windows platform...");
                if (line.indexOf("platform Windows") > 0) {
                    if (DEBUG.IO)
                        Log.debug(url + " was saved in the Windows environment");
                    savedOnWindowsPlatform = true;
                } else if (line.indexOf("platform Mac") > 0) {
                    if (DEBUG.IO)
                        Log.debug(url + " was saved in the Mac environment");
                    savedOnMacPlatform = true;
                }
            } else if (line.startsWith(VUE_COMMENT_START + " Saving version")) {
                if (DEBUG.IO)
                    Log.debug("Found saving version line: " + line);
                final int savingVersionIndex = line.indexOf("VUE");
                if (savingVersionIndex > 0) {
                    savingVersion = line.substring(line.indexOf("VUE"), line.length());
                    if (savingVersion.indexOf("-->") > 10)
                        savingVersion = savingVersion.substring(0, savingVersion.indexOf("-->"));
                    savingVersion = savingVersion.trim();
                } else {
                    Log.warn(url + ": unknown saving version XML comment [" + line + "]");
                }
                if (DEBUG.IO)
                    Log.debug("Saving version: [" + savingVersion + "]");
            }

            // Scan the comment line for a version tag to base our mapping on:
            int idx;
            if ((idx = line.indexOf("@version(")) >= 0) {
                String s = line.substring(idx);
                //System.out.println("Found version start:" + s);
                int x = s.indexOf(')');
                if (x > 0) {
                    versionID = s.substring(9, x);
                    if (DEBUG.CASTOR || DEBUG.IO)
                        Log.debug(url + "; Found mapping version ID[" + versionID + "]");
                    if (versionID.equals(XML_MAPPING_CURRENT_VERSION_ID)) {
                        mapping = getDefaultMapping();
                    } else {
                        URL mappingURL = VueResources.getURL("mapping.lw.version_" + versionID);
                        if (mappingURL == null) {
                            Log.error("Failed to find mapping for version tag [" + versionID
                                    + "], attempting default.");
                            mapping = getDefaultMapping();
                        } else {
                            mapping = getMapping(mappingURL);
                        }
                    }
                }
            }
        }
        reader.close();

        if (firstNonCommentLine.startsWith("<?xml")) {
            // Check to see if we need to use a non-default input encoding.
            // NOTE: We make sure we only attempt guessedEncoding if the given encoding is
            // the default input encoding: otherwise assume we're here recursively,
            // after already guessing at an encoding (otherwise, we'll loop, and blow stack)
            if (DEBUG.IO)
                Log.debug("XML head [" + firstNonCommentLine + "]");

            if (firstNonCommentLine.indexOf("encoding=\"UTF-8\"") > 0) {

                boolean localEncoding = false;

                // If encoding is UTF-8, this a 2nd generation save file (mapping is
                // versioned, but not all US-ASCII encoding): the actual encoding is
                // unknown: make a guess as how to best handle it.  This is our rule: if
                // we're on the SAME platform as the save file, assuming the local
                // encoding (assume it's the same user, on the same machine, and the
                // current default system encoding is the same one that was active when
                // the file was originally saved).  If we're on a different platform,
                // assume a default encoding for that platform.

                if (Util.isWindowsPlatform()) {
                    if (savedOnWindowsPlatform) {
                        localEncoding = true;
                    } else if (savedOnMacPlatform)
                        guessedEncoding = DEFAULT_MAC_ENCODING;
                    else
                        guessedEncoding = DEFAULT_WINDOWS_ENCODING;
                } else if (Util.isMacPlatform()) {
                    if (savedOnMacPlatform)
                        localEncoding = true;
                    else if (savedOnWindowsPlatform)
                        guessedEncoding = DEFAULT_WINDOWS_ENCODING;
                    else
                        guessedEncoding = DEFAULT_MAC_ENCODING;
                }

                if (localEncoding)
                    guessedEncoding = Util.getDefaultPlatformEncoding();

                Log.info(url + "; assuming " + (localEncoding ? "LOCAL " : "PLATFORM DEFAULT ") + "\'"
                        + guessedEncoding + "\' charset encoding");

                // Note: doing this is a real tradeoff amongst bugs: any old save file
                // that had fancy unicode characters in UTF, such as something in a
                // japanese charset, will be screwed by this, so we optimizing for what
                // we think is the most likely case.  if this becomes a real problem, we
                // could introduce a special convert dialog.  Also, pre US-ASCII save
                // files could have different strings in them saved in many DIFFERENT
                // charsets (e.g., japanese, UTF, etc), and it's complete luck as to
                // when those charsets would each be properly handled.

            }
        } else {
            Log.warn("Missing XML header in [" + firstNonCommentLine + "]");
        }

        boolean oldFormat = false;

        if (versionID == null && mapping == null) {
            oldFormat = true;
            Log.info(url + "; save file is of old pre-versioned type.");
            mapping = getMapping(XML_MAPPING_UNVERSIONED);
        }

        if (mapping == null)
            mapping = getDefaultMapping();

        final String encoding = guessedEncoding == null ? DEFAULT_INPUT_ENCODING : guessedEncoding;

        return unmarshallMap(url, mapping, encoding, oldFormat, savingVersion, handler);
    }

    private static LWMap unmarshallMap(final java.net.URL url, Mapping mapping, String charsetEncoding,
            boolean allowOldFormat, String savingVersion, MapUnmarshalHandler mapHandler)
            //throws IOException, org.exolab.castor.mapping.MappingException, org.exolab.castor.xml.ValidationException
            throws IOException {
        LWMap map = null;

        Log.info("unmarshalling: " + url + "; charset=" + charsetEncoding);

        // TODO: now that we support opening maps via HTTP URL's, it's a bit obscene to
        // open the URL twice just to support old maps where we might need to detect the
        // encoding, then re-open the file with the proper encoding.  We could
        // presumably have the MapReader keep an underlying buffered raw InputStream,
        // then create Reader's on top of that with different encodings to support this
        // more smoothly.  SMF 2008-04-08
        final MapReader mapReader = getMapReaderForURL(url, charsetEncoding, true);

        final BufferedReader reader = mapReader.reader;

        // Skip over comments to get to start of XML

        for (;;) {
            reader.mark(2048); // a single comment line can't be longer than this...
            String line = reader.readLine();
            if (line == null) {
                Log.error("Unexpected end-of-stream in [" + url + "]");
                throw new java.io.IOException("end of stream in " + url);
            }
            if (line.startsWith("<!--") == false) {
                // we should have just hit thie "<?xml ..." line -- done with comments
                break;
            }
            if (DEBUG.META && (DEBUG.CASTOR || DEBUG.IO))
                Log.debug("Skipping[" + line + "]");
        }

        // Reset the reader to the start of the last line read, which should be the <?xml line,
        // which is what castor needs to see at start (it can't handle ignoring comments...)
        reader.reset();

        final String sourceName = url.toString();

        try {
            Unmarshaller unmarshaller = getDefaultUnmarshaller(mapping, sourceName);

            if (mapHandler == null)
                mapHandler = new MapUnmarshalHandler(url, tufts.vue.Resource.MANAGED_UNMARSHALLING); // managed is the default

            if (DEBUG.Enabled)
                Log.debug("unmarshal handler: " + mapHandler);
            unmarshaller.setUnmarshalListener(mapHandler);

            // unmarshall the map:

            try {
                map = (LWMap) unmarshaller.unmarshal(new InputSource(reader));
                //} catch (org.exolab.castor.xml.MarshalException me) {
            } catch (org.exolab.castor.xml.MarshalException me) {
                //if (allowOldFormat && me.getMessage().endsWith("tufts.vue.Resource")) {
                //if (allowOldFormat && me.getMessage().indexOf("Unable to instantiate tufts.vue.Resource") >= 0) {
                // 2007-10-01 SMF: rev forward the special exception to check for once again in new castor version: castor-1.1.2.1-xml.jar
                // TODO: 2009-03-25: upgraded to Castor release 1.3: the below message check may no longer work...
                if (allowOldFormat && me.getMessage() != null
                        && me.getMessage().indexOf("tufts.vue.Resource can no longer be constructed") >= 0) {
                    Log.warn("ActionUtil.unmarshallMap: " + me);
                    Log.warn("Attempting specialized MapResource mapping for old format.");
                    // NOTE: delicate recursion here: won't loop as long as we pass in a non-null mapping.
                    return unmarshallMap(url, getMapping(XML_MAPPING_OLD_RESOURCES), charsetEncoding, false,
                            savingVersion, mapHandler);
                } else
                    throw me;
            }

            reader.close();

            Log.info("unmarshalled: " + map);

            // The below three notify calls must be called in exact sequence (file, then version, then completed)

            mapHandler.notifyFile(map, mapReader.file);

            mapHandler.notifyVersionOfVueThatSavedMap(savingVersion);

            mapHandler.notifyUnmarshallingCompleted();

            Log.debug("completed: " + map);
        } catch (Exception e) {
            tufts.Util.printStackTrace(e, "Exception restoring map from [" + url + "]: " + e.getClass().getName());
            Log.info("map-as-is: " + Util.tags(map)); // presumably null
            map = null;
            throw new Error("Exception restoring map from [" + url + "]", e);
        }

        return map;
    }

    /**
     * This class is only there to provide something for the old mapping description for LWMergeMap to refer
     * to. XMLObjectFactory will turn it into an LWMap.  For the re-mapping to work, it must be a subclass of LWMap.
     * Castor should never need to actuall instance or load this class.
     */
    public static final class OLD_MERGE_MAP_STUB extends tufts.vue.LWMap {
        static {
            if (true)
                throw new Error("should-not-classload");
        }
    }

}

/**
 * This class serves as both an UnmarshalListener impl for VUE LWMap's and their components,
 * as well as a handles what to do to / with a map once it's been unmarshalled to get
 * it into it's final, completed state (e.g., in some cases, transformations may need to be performed).
 */

class MapUnmarshalHandler implements UnmarshalListener {

    public static final Object CONTEXT_NONE = "NONE";

    private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(MapUnmarshalHandler.class);

    final Object source;
    final Object context;

    protected LWMap map;
    protected File file;

    MapUnmarshalHandler(Object source, Object context) {
        this.source = source;
        if (context == null)
            this.context = CONTEXT_NONE;
        else
            this.context = context;
    }

    /** This must be called first */
    void notifyFile(final LWMap map, final File file) {
        this.map = map;
        this.file = file;

        map.setFile(file); // VUE-713: do this always

        // Note: LWMap.setFile sets the map label

    }

    /** This must be called in sequence after notifyFile */
    void notifyVersionOfVueThatSavedMap(String savingVersion) {
        final String fileName = file.getName();

        if (map.getModelVersion() > LWMap.getCurrentModelVersion()) {
            VueUtil.alert(
                    String.format(Locale.getDefault(),
                            VueResources.local("actionutil.notifyversion.message") + "\n\n"
                                    + VueResources.local("actionutil.notifyversion.datamodel") + " \n",
                            file, map.getModelVersion(), LWMap.getCurrentModelVersion()) + "\n"
                            + VueResources.local("actionutil.notifyversion.savedversion") + "\n" + savingVersion
                            + "\n" + VueResources.local("actionutil.notifyversion.currentversion") + "\n"
                            + "        " + VueResources.local("actionutil.notifyversion.vuebuilt") + " "
                            + tufts.vue.Version.AllInfo + " (public v" + VueResources.local("vue.version") + ")"
                            + "\n" + "\n" + VueResources.local("actionutil.notifyversion.displaymap") + "\n"
                            + VueResources.local("actionutil.notifyversion.corruptmap"),
                    String.format(Locale.getDefault(),
                            VueResources.local("actionutil.notifyversion.versionwarning"), fileName));

            map.setLabel(fileName + " (as available)");
            // Skip setting the file: this will force save-as if they try to save.
        } else {

            // VUE-713: don't do this conditionallly
            //                 // This setFile also sets the label name, so it appears as a modification in the map.
            //                 // So be sure to do completeXMLRestore last, as it will reset the modification count.
            //                 if (map.getModelVersion() < 1) {
            //                     map.setLabel(file.getName());
            //                     // force save as for old maps as they will no longer work in old stable versions of VUE (1.5 & prior)
            //                     // if they're saved in this new version of VUE.
            //                 } else {
            //                     map.setFile(file);
            //                 }

            if (DEBUG.DATA && DEBUG.META)
                map.setLabel("|" + map.getModelVersion() + "| " + map.getLabel());
        }

        Log.debug("label-set: " + map);

    }

    /** This must be called last */
    void notifyUnmarshallingCompleted() {
        // note that map.setFile should normally have been completed before this is called
        map.completeXMLRestore(context);
    }

    /** @see org.exolab.castor.xml.UnmarshalListener */
    public void initialized(Object o) {
        if (DEBUG.XML && DEBUG.META)
            Log.debug(" initialized: " + Util.tags(o));
        if (o instanceof XMLUnmarshalListener) {
            try {
                ((XMLUnmarshalListener) o).XML_initialized(context);
            } catch (Throwable t) {
                Log.error(this, t);
            }
        }
    }

    /** @see org.exolab.castor.xml.UnmarshalListener */
    public void attributesProcessed(Object o) {
        if (DEBUG.XML && DEBUG.META)
            Log.debug("  attributes: " + Util.tags(o));
    }

    /** @see org.exolab.castor.xml.UnmarshalListener */
    public void unmarshalled(Object o) {
        if (DEBUG.XML && DEBUG.META)
            Log.debug("unmarshalled: " + Util.tags(o));

        if (o instanceof XMLUnmarshalListener) {
            try {
                ((XMLUnmarshalListener) o).XML_completed(context);
            } catch (Throwable t) {
                Log.error(this, t);
            }
        }
    }

    /** @see org.exolab.castor.xml.UnmarshalListener */
    public void fieldAdded(String name, Object parent, Object child) {
        if (DEBUG.XML) {

            //final String field = "child[" + Util.TERM_YELLOW + name + Util.TERM_CLEAR + "] ";
            //final String field = Util.TERM_YELLOW + name + Util.TERM_CLEAR + ": ";
            final String field = Util.tag(parent) + "/" + Util.TERM_YELLOW + name + Util.TERM_CLEAR + " = ";

            if (DEBUG.META) {
                Log.debug("  fieldAdded: parent: " + Util.tags(parent) + " " + field + Util.tags(child) + "\n");
                //System.out.println("VUL fieldAdded: parent: " + parent.getClass().getName() + "\t" + tos(parent) + "\n"
                //+ "             new child: " +  child.getClass().getName() + " \"" + name + "\" " + tos(child) + "\n");
            } else {
                if (child instanceof String)
                    Log.debug(field + Util.TERM_RED + '"' + child + '"' + Util.TERM_CLEAR);
                else
                    Log.debug(field + Util.TERM_PURPLE + Util.tags(child) + Util.TERM_CLEAR);
                //Log.debug(field + Util.tags(child));
            }
        }
        if (parent instanceof XMLUnmarshalListener) {
            try {
                ((XMLUnmarshalListener) parent).XML_fieldAdded(context, name, child);
            } catch (Throwable t) {
                Log.error(this, t);
            }
        }
        if (child instanceof XMLUnmarshalListener) {
            try {
                ((XMLUnmarshalListener) child).XML_addNotify(context, name, parent);
            } catch (Throwable t) {
                Log.error(this, t);
            }
        }
    }

    public String toString() {
        return getClass().getName() + "[" + context + "; " + source + "]";
    }

    //         // exception trapping toString in case the object isn't initialized enough
    //         // for it's toString to work...
    //         private String tos(Object o) {
    //             if (o == null)
    //                 return "<null-object>";

    //             String s = o.getClass().getName() + " ";
    //             //String s = null;
    //             String txt = null;
    //             try {
    //                 txt = o.toString();
    //                 if (
    //             } catch (Throwable t) {
    //                 txt = t.toString();
    //                 // "[" + t.toString() + "]";
    //             }
    //             return s;
    //         }

}

final class XMLObjectFactory extends org.exolab.castor.util.DefaultObjectFactory {

    private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(XMLObjectFactory.class);

    final Object source;

    XMLObjectFactory(Object source) {
        this.source = source;
        if (DEBUG.Enabled)
            Log.debug("new " + Util.tags(this) + "; " + source);
    }

    @Override
    public Object createInstance(Class type, Object[] args) throws IllegalAccessException, InstantiationException {
        //System.err.println("VOF0 ASKED FOR " + type + " args=" + args);
        Log.info("*** ASKED FOR " + type + " args=" + args); // apparently never called
        return this.createInstance(type, null, null);
    }

    @Override
    public Object createInstance(Class type) throws IllegalAccessException, InstantiationException {
        //System.err.println("VOF1 ASKED FOR " + type);
        Log.info("*** ASKED FOR " + type); // apparently never called
        return this.createInstance(type, null, null);
    }

    @Override
    public Object createInstance(final Class _type, final Class[] argTypes, final Object[] args)
            throws IllegalAccessException, InstantiationException {

        final Class type;
        if (_type == ActionUtil.OLD_MERGE_MAP_STUB.class) {
            type = tufts.vue.LWMap.class;
        }
        // else if (_type == tufts.vue.MapResource.class || _type == tufts.vue.CabinetResource.class)
        //     type = tufts.vue.URLResource.class;
        else
            type = _type;

        if (_type != type)
            Log.info("request for instance of " + _type.getName() + " yields: " + type);

        //System.err.println("VOF ASKED FOR " + type + " argTypes=" + argTypes);
        //Object o = super.createInstance(type);
        final Object o = type.newInstance();
        if (DEBUG.Enabled) {
            if ((DEBUG.IO && DEBUG.META) || DEBUG.XML || DEBUG.CASTOR
                    || (DEBUG.RESOURCE && DEBUG.META && o instanceof tufts.vue.Resource)) {

                // Do not use Util.tags(o) or allow toString to be called on object -- unmarshalling can fail
                // if there are side-effects (!!!) due to calling it -- this happened at one point with an
                // instance of FavoritesDataSource.  Util.tag(o) is fine tho.

                Log.debug("+= " + Util.tag(o));
            }
        }
        return o;
    }
}

class MapException extends IOException {
    public MapException(String s) {
        super(s);
    }
}

class EmptyFileException extends IOException {
    EmptyFileException() {
        super("Empty file (zero length); no VUE data here");
    }
}

class PaddedCellRenderer extends DefaultListCellRenderer {
    public Component getListCellRendererComponent(JList list, Object value, // value to display
            int index, // cell index
            boolean iss, // is selected
            boolean chf) // cell has focus?
    {
        super.getListCellRendererComponent(list, value, index, iss, chf);

        setText(((VueFileFilter) value).getDescription());
        this.setBorder(BorderFactory.createEmptyBorder(1, 5, 1, 1));

        return this;
    }
}