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