/*******************************************************************************
* CogTool Copyright Notice and Distribution Terms
* CogTool 1.3, Copyright (c) 2005-2013 Carnegie Mellon University
* This software is distributed under the terms of the FSF Lesser
* Gnu Public License (see LGPL.txt).
*
* CogTool is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* CogTool is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CogTool; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* CogTool makes use of several third-party components, with the
* following notices:
*
* Eclipse SWT version 3.448
* Eclipse GEF Draw2D version 3.2.1
*
* Unless otherwise indicated, all Content made available by the Eclipse
* Foundation is provided to you under the terms and conditions of the Eclipse
* Public License Version 1.0 ("EPL"). A copy of the EPL is provided with this
* Content and is also available at http://www.eclipse.org/legal/epl-v10.html.
*
* CLISP version 2.38
*
* Copyright (c) Sam Steingold, Bruno Haible 2001-2006
* This software is distributed under the terms of the FSF Gnu Public License.
* See COPYRIGHT file in clisp installation folder for more information.
*
* ACT-R 6.0
*
* Copyright (c) 1998-2007 Dan Bothell, Mike Byrne, Christian Lebiere &
* John R Anderson.
* This software is distributed under the terms of the FSF Lesser
* Gnu Public License (see LGPL.txt).
*
* Apache Jakarta Commons-Lang 2.1
*
* This product contains software developed by the Apache Software Foundation
* (http://www.apache.org/)
*
* jopt-simple version 1.0
*
* Copyright (c) 2004-2013 Paul R. Holser, Jr.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Mozilla XULRunner 1.9.0.5
*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (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.mozilla.org/MPL/.
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
* The J2SE(TM) Java Runtime Environment version 5.0
*
* Copyright 2009 Sun Microsystems, Inc., 4150
* Network Circle, Santa Clara, California 95054, U.S.A. All
* rights reserved. U.S.
* See the LICENSE file in the jre folder for more information.
******************************************************************************/
package edu.cmu.cs.hcii.cogtool.util;
import java.io.Writer;
import java.lang.reflect.Array;
import java.rmi.UnexpectedException;
import java.util.Collection;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.ecf.core.util.Base64;
/**
* Class to support the serialization of normal Java objects into
* an XML stream, suitable for saving persistently.
* <p>
* Every object must register a saver (see, for example,
* <code>CogToolSerialization</code>).
* <p>
* This architecture handles objects that are array types or that subclass
* <code>Collection</code> and <code>Map</code>. However, it does *not*
* handle <code>Map</code> key values that themselves are aggregates.
* <p>
* All "primitive" values (int, double, char, boolean, their "object"
* counterparts, and String) are stored directly as base cases. Enumeration
* values are determined using the <code>isEnum</code> method of the
* class' saver object and the representation stored is based on the
* enum "code" (we currently assume that the enumeration class subclasses
* <code>Enumerated</code>.
* <p>
* The serialization of an object class will store the information held
* by the class' superclass if the superclass has also registered a saver.
* Once a superclass is encountered that has not registered saver, no further
* attempts are made, even if some ancestor superclass has registered a saver.
* <p>
* Serializations can be for several purposes; an object class may
* choose to save itself differently for different purposes.
* <p>
* Multiple objects can be saved in a single serialization. The
* <code>finish</code> method should be invoked when all objects of interest
* have been saved. If the <code>Writer</code> involves any system resources
* that should be recovered, the appropriate method should also then be called
* (such as the <code>close()</code> method for <code>FileWriter</code>).
*
* @author mlh
*/
public class ObjectSaver implements ObjectPersist
{
/**
* A class' saver informs us about the version of the serialization
* that will be produced, whether the value will represent an enumeration,
* and how to serialize the "persistent" data of the value.
*
* @author mlh
*/
public interface IDataSaver<T>
{
/**
* Return the format version for this serialization of the object.
*
* @return the format version for this serialization of the object
* @author mlh
*/
public int getVersion();
/**
* Return whether the value represents an enumeration element.
*
* @return true if and only if the value represents an enumeration
* @author mlh
*/
public boolean isEnum(); // if so, saveData() is not interesting
/**
* Serialize the data of the given object value using the given saver.
*
* @param v the object to serialize
* @param saver the "destination" of the serialization
* @throws java.io.IOException if the saver's sink generates one
* during calls to <code>write</code>
* @author mlh
*/
public void saveData(T v, ObjectSaver saver)
throws java.io.IOException;
}
/**
* This represents a default implementation of a saver that assumes
* the value is not an enumeration and otherwise throws exceptions
* to indicate unsupported operations.
*
* @author mlh
*/
public static class ADataSaver<T> implements IDataSaver<T>
{
public boolean isEnum()
{
return false;
}
public void saveData(T v, ObjectSaver saver)
throws java.io.IOException
{
throw new UnsupportedOperationException("saveData");
}
public int getVersion()
{
throw new UnsupportedOperationException("getVersion");
}
}
public interface ISaverRegistry
{
/**
* Every object must register a saver that specifies how to serialize
* its value.
*
* @param className the name of the class; this should almost always be
* the value of <code>C.class.getName()</code>
* @param saver the saver object that controls how to serialize a value
* of the given type
* @author mlh
*/
public <E> void registerSaver(String className, IDataSaver<E> saver);
/**
* Every object must register a saver that specifies how to serialize
* its value. This method returns the saver associated with the given
* class name.
*
* @param className the name of the class; this should almost always be
* the value of <code>C.class.getName()</code>
* @return the saver object that controls how to serialize a value of
* the given type, or <code>null</code> if none was registered
* @author mlh
*/
public IDataSaver<?> getSaver(String className);
}
/**
* Default implementation, especially for the default registry.
*/
public static class DefaultSaverRegistry implements ISaverRegistry
{
// registry of IDataSavers mapping
// (class-name + '#' + version) to IDataSaver
protected Map<String, IDataSaver<?>> saverRegistry =
new HashMap<String, IDataSaver<?>>();
public <E> void registerSaver(String className, IDataSaver<E> saver)
{
saverRegistry.put(className, saver);
}
public IDataSaver<?> getSaver(String className)
{
return saverRegistry.get(className);
}
}
/**
* Allows the caller to provide different behavior for a subset of
* objects, maintaining "normal" behavior for other objects.
*/
public static class OverrideSaverRegistry extends DefaultSaverRegistry
{
protected ISaverRegistry overriddenRegistry;
public OverrideSaverRegistry(ISaverRegistry overridden)
{
overriddenRegistry = overridden;
}
@Override
public IDataSaver<?> getSaver(String className)
{
IDataSaver<?> saver = super.getSaver(className);
if (saver != null) {
return saver;
}
return overriddenRegistry.getSaver(className);
}
}
/**
* Default registry for savers.
*/
public static final ISaverRegistry DEFAULT_REGISTRY =
new DefaultSaverRegistry();
// Registry to use for the saving process
protected ISaverRegistry saverRegistry;
// maps Object to id
protected Map<Object, Integer> savedObjects =
new IdentityHashMap<Object, Integer>();
// The sink receives the XML serialization.
protected Writer sink;
// The indentation is based on the nesting level of the object "hierarchy"
// TODO: Make this into a StringBuilder
protected String indent = "";
// For generating unique id's (see savedObjects above)
protected int idGen = 1;
// Serializations can be for several purposes; an object class may
// choose to save itself differently for different purposes.
protected Object purpose;
/**
* The default purpose for serialization.
*/
public static final Object DEFAULT_PURPOSE = null;
/**
* A constructor using the default "purpose". The serialization will be
* "saved" to the given sink. When all objects of interest have been saved,
* the <code>finish()</code> method should be invoked. If the
* <code>Writer</code> involves any system resources that should be
* recovered, the appropriate method should also then be called
* (such as the <code>close()</code> method for <code>FileWriter</code>).
*
* @param objectSink the sink that will accept the serialization as
* it is generated
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public ObjectSaver(Writer objectSink)
throws java.io.IOException
{
this(objectSink, null);
}
/**
* A constructor for a specific "purpose". The serialization will be
* "saved" to the given sink. When all objects of interest have been
* saved, the <code>finish()</code> method should be invoked. If the
* <code>Writer</code> involves any system resources that should be
* recovered, the appropriate method should also then be invoked by the
* caller (such as <code>close()</code> for a <code>FileWriter</code>).
*
* @param objectSink the sink that will accept the serialization as
* it is generated
* @param savePurpose the purpose for the serialization; this may be used
* by object savers to alter exactly what is serialized
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public ObjectSaver(Writer objectSink, Object savePurpose)
throws java.io.IOException
{
this(objectSink, savePurpose, null);
}
/**
* A constructor for a specific "purpose". The serialization will be
* "saved" to the given sink. When all objects of interest have been
* saved, the <code>finish()</code> method should be invoked. If the
* <code>Writer</code> involves any system resources that should be
* recovered, the appropriate method should also then be invoked by the
* caller (such as <code>close()</code> for a <code>FileWriter</code>).
*
* @param objectSink the sink that will accept the serialization as
* it is generated
* @param savePurpose the purpose for the serialization; this may be used
* by object savers to alter exactly what is serialized
* @param registry the saver registry to use for fetching the IDataSaver
* instances for saving each object
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public ObjectSaver(Writer objectSink,
Object savePurpose,
ISaverRegistry registry)
throws java.io.IOException
{
saverRegistry = (registry != null) ? registry : DEFAULT_REGISTRY;
sink = objectSink;
purpose = savePurpose;
// Start the outermost XML element
sink.write("<" + PERSIST_ELT
+ addAttribute(VERSION_ATTR,
Integer.toString(FORMAT_VERSION))
+ addAttribute("cogtool_version",
System.getProperty("cogtool.version"))
+ addAttribute("cogtool_revision",
System.getProperty("cogtool.revision"))
+ addAttribute("cogtool_buildtime",
System.getProperty("cogtool.build"))
+ addAttribute("java_version",
System.getProperty("java.version"))
+ addAttribute("os_version",
System.getProperty("os.version"))
+ addAttribute("os_name",
System.getProperty("os.name"))
+ ">\n");
}
/**
* Return the registry associated with this saver.
*/
public ISaverRegistry getRegistry()
{
return saverRegistry;
}
/**
* Terminates the serialization and flushes the output sink.
*
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code> or <code>flush</code>
* @author mlh
*/
public void finish()
throws java.io.IOException
{
sink.write("</" + PERSIST_ELT + ">\n");
sink.flush();
}
/**
* Return the "purpose" for this serialization; an object class may
* choose to save itself differently for different purposes.
*
* @return the purpose given on construction
* @author mlh
*/
public Object getPurpose()
{
return purpose;
}
/**
* Every object must register a saver that specifies how to serialize
* its value.
*
* @param className the name of the class; this should almost always be
* the value of <code>C.class.getName()</code>
* @param saver the saver object that controls how to serialize a value
* of the given type
* @author mlh
*/
public static <T> void registerSaver(String className, IDataSaver<T> saver)
{
DEFAULT_REGISTRY.registerSaver(className, saver);
}
/**
* This method quotes the given string so that it results in a valid
* XML attribute value (e.g., double quotes are represented by the
* appropriate XML entity).
*
* @param str the string to be quoted
* @return the quoted string
* @author mlh
*/
protected String quote(String str)
{
// TODO: needs something like PHP's htmlentities in Java
return (str != null) ? str.replaceAll("\"", """) : "";
}
/**
* This method returns the proper form of an XML element attribute
* with the given name and value.
*
* @param attr the attribute name
* @param value the attribute value
* @return the substring representing the proper form of an XML attribute
* @author mlh
*/
protected String addAttribute(String attr, String value)
{
return " " + attr + "=\"" + quote(value) + "\"";
}
/**
* This method produces an XML element attribute for the "variable" name
* if the name is specified (that is, not <code>null</code>).
*
* @param variable the name of the variable attribute;
* may be <code>null</code>
* @return the string representing the "variable" XML attribute if the
* given string is not <code>null</code>;
* the empty string otherwise
* @author mlh
*/
protected String addVariableAttribute(String variable)
{
return (variable != null) ? addAttribute(VAR_ATTR, variable) : "";
}
/**
* Method to save a primitive <code>int</code> value that is either
* a top-level value or an element in an array, <code>Map</code>, or
* <code>Collection</code>, but not an instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveInt(int value)
throws java.io.IOException
{
saveInt(value, null);
}
/**
* Method to save a primitive <code>long</code> value that is either
* a top-level value or an element in an array, <code>Map</code>, or
* <code>Collection</code>, but not an instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveLong(long value)
throws java.io.IOException
{
saveLong(value, null);
}
/**
* Method to save a primitive <code>double</code> value that is either
* a top-level value or an element in an array, <code>Map</code>, or
* <code>Collection</code>, but not an instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveDouble(double value)
throws java.io.IOException
{
saveDouble(value, null);
}
/**
* Method to save a primitive <code>boolean</code> value that is either
* a top-level value or an element in an array, <code>Map</code>, or
* <code>Collection</code>, but not an instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveBoolean(boolean value)
throws java.io.IOException
{
saveBoolean(value, null);
}
/**
* Method to save a <code>String</code> value that is either
* a top-level value or an element in an array, <code>Map</code>, or
* <code>Collection</code>, but not an instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the value to serialize
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveString(String value)
throws java.io.IOException
{
saveString(value, null);
}
/**
* Method to save a primitive <code>char</code> value that is either
* a top-level value or an element in an array, <code>Map</code>, or
* <code>Collection</code>, but not an instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveChar(char value)
throws java.io.IOException
{
saveChar(value, null);
}
/**
* Method to save an object that is either
* a top-level value or an element in an array, <code>Map</code>, or
* <code>Collection</code>, but not an instance variable of another object.
* To be successful, a saver for the value's class must have been
* previously registered.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the value to serialize
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @throws UnexpectedException if no saver has been registered for the
* value's class
* @author mlh
*/
public void saveObject(Object value)
throws java.io.IOException
{
saveObject(value, null);
}
/**
* Method to save a primitive <code>int</code> value that is an
* instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, <code>Map</code>,
* or <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveInt(int value, String variable)
throws java.io.IOException
{
sink.write(indent
+ "<" + INT_ELT
+ addVariableAttribute(variable)
+ addAttribute(VALUE_ATTR, Integer.toString(value))
+ "/>\n");
}
/**
* Method to save a primitive <code>long</code> value that is an
* instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, <code>Map</code>,
* or <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveLong(long value, String variable)
throws java.io.IOException
{
sink.write(indent
+ "<" + LONG_ELT
+ addVariableAttribute(variable)
+ addAttribute(VALUE_ATTR, Long.toString(value))
+ "/>\n");
}
/**
* Method to save a primitive <code>double</code> value that is an
* instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, <code>Map</code>,
* or <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveDouble(double value, String variable)
throws java.io.IOException
{
sink.write(indent
+ "<" + DOUBLE_ELT
+ addVariableAttribute(variable)
+ addAttribute(VALUE_ATTR, Double.toString(value))
+ "/>\n");
}
/**
* Method to save a primitive <code>boolean</code> value that is an
* instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, <code>Map</code>,
* or <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveBoolean(boolean value, String variable)
throws java.io.IOException
{
sink.write(indent
+ "<" + BOOL_ELT
+ addVariableAttribute(variable)
+ addAttribute(VALUE_ATTR,
(value ? BOOL_TRUE : BOOL_FALSE))
+ "/>\n");
}
private static String LEGAL_LOW_CHARACTERS = "\t\n\r";
/**
* Method to save a <code>String</code> value that is an
* instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, <code>Map</code>,
* or <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveString(String value, String variable)
throws java.io.IOException
{
if (value == null) {
sink.write(indent
+ "<" + NULL_ELT
+ addVariableAttribute(variable)
+ "/>\n");
}
else {
// TODO perhaps this shouldn't be here in perpetuity; it's been
// added to try to track down an infrequently recurring
// problem where one user sometimes gets a Unit Separator
// character in a display label, which then makes the SAX
// parser go south when trying to read the resulting file.
for (char c : value.toCharArray()) {
if (c < ' ' && LEGAL_LOW_CHARACTERS.indexOf(c) < 0) {
throw new IllegalStateException(String.format(
"Unexpected character (%d decimal) encountered when writing file",
(int)c));
}
}
// SAX parser incorrectly assumes a ']' at the end of our string value
// introduces end-of-CDATA; thus, always append '@' to prevent the bug.
sink.write(indent
+ "<" + STR_ELT
+ addVariableAttribute(variable)
+ addAttribute(SIZE_ATTR,
Integer.toString(value.length()))
+ "><![CDATA["
+ value + "@"
+ "]]></" + STR_ELT + ">\n");
}
}
/**
* Method to save a primitive <code>char</code> value that is an
* instance variable of an object.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the primitive value to serialize
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, <code>Map</code>,
* or <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public void saveChar(char value, String variable)
throws java.io.IOException
{
// SAX parser incorrectly assumes a character value of ']'
// introduces end-of-CDATA; thus, always append '@' to prevent the bug.
sink.write(indent
+ "<" + CHAR_ELT
+ addVariableAttribute(variable)
+ "><![CDATA["
+ Character.toString(value) + "@"
+ "]]></" + CHAR_ELT + ">\n");
}
/**
* Method to serialize and save a previously saved object as a reference.
* Each object is assigned a unique id and registered in
* <code>savedObjects</code>. If <code>saveObject</code> sees a previously
* saved object, a reference is generated instead of saving it out again.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param id the generated unique identifier for the object
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, <code>Map</code>,
* or <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
protected void saveReference(Integer id, String variable)
throws java.io.IOException
{
sink.write(indent + "<" + REF_ELT
+ addVariableAttribute(variable)
+ addAttribute(IDREF_ATTR, id.toString())
+ "/>\n");
}
/**
* Method to serialize and save an enumeration value. The serialization
* of an enumeration value consists solely of its "persistence" enum code.
* It is the responsibility of the loader that deserializes this value
* to return the unique (singleton) enumeration value based on that code.
* To support evolution of enumeration values, the enumeration class'
* name and serialization version is part of the value's serialization.
* <p>
* NOTE: We currently assume that the enumeration class subclasses
* <code>Enumerated</code>.
* <p>
* The XML format will be as described in ObjectPersist.java.
* <p>
* Note that enumeration values do not get assigned unique id's because
* they are effectively unique (like singleton) values.
*
* @param className the name of the enumeration class
* @param saver the saver registered for the given class
* @param value the enumeration value to serialize; as noted, we expect the
* class of this value to subclass <code>Enumerated</code>
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, <code>Map</code>,
* or <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
protected void saveEnum(String className,
IDataSaver<?> saver,
Object value,
String variable)
throws java.io.IOException
{
String version = Integer.toString(saver.getVersion());
String enumCode = ((Enumerated) value).persistenceValue();
sink.write(indent + "<" + ENUM_ELT
+ addVariableAttribute(variable)
+ addAttribute(VALUE_ATTR, enumCode)
+ addAttribute(CLASS_ATTR, className)
+ addAttribute(VERSION_ATTR, version)
+ "/>\n");
}
/**
* This method saves the given object that must be an instance of an array
* class. Each member of the array is recursively serialized/saved.
* To support evolution of array types, the array elements' base class
* name is part of the array value's serialization. Since array objects
* can be shared, a unique id should be generated for this value so that
* it will be serialized only once.
* <p>
* Arrays of type <code>byte[]</code> are stored specially using
* byte64 encoding.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param valueClass the name of the base class for the array's elements
* @param value the array object itself
* @param id the generated unique identifier for the array object
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, <code>Map</code>,
* or <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
protected void saveArray(Class<?> valueClass,
Object value,
Integer id,
String variable)
throws java.io.IOException
{
Class<?> eltType = valueClass.getComponentType();
// Check if the array is byte[]
if (eltType == Byte.TYPE) {
String encoded = Base64.encode((byte[]) value);
sink.write(indent + "<" + BYTES_ELT
+ addVariableAttribute(variable)
+ addAttribute(ID_ATTR, id.toString())
+ addAttribute(SIZE_ATTR,
Integer.toString(encoded.length()))
+ ">");
sink.write(encoded);
sink.write("</" + BYTES_ELT + ">\n");
}
else {
int count = Array.getLength(value);
sink.write(indent + "<" + ARRAY_ELT
+ addVariableAttribute(variable)
+ addAttribute(ID_ATTR, id.toString())
+ addAttribute(CLASS_ATTR,
eltType.getName())
+ addAttribute(SIZE_ATTR,
Integer.toString(count))
+ ">\n");
String oldIndent = indent;
indent += " ";
// Serialize element values recursively as efficiently as possible.
if (eltType.isPrimitive()) {
if (eltType == Integer.TYPE) {
for (int i = 0; i < count; i++) {
saveInt(Array.getInt(value, i));
}
}
else if (eltType == Long.TYPE) {
for (int i = 0; i < count; i++) {
saveLong(Array.getLong(value, i));
}
}
else if (eltType == Double.TYPE) {
for (int i = 0; i < count; i++) {
saveDouble(Array.getDouble(value, i));
}
}
else if (eltType == Boolean.TYPE) {
for (int i = 0; i < count; i++) {
saveBoolean(Array.getBoolean(value, i));
}
}
else if (eltType == Character.TYPE) {
for (int i = 0; i < count; i++) {
saveChar(Array.getChar(value, i));
}
}
}
else {
// Element values are String or other objects;
// recursively serialize.
for (int i = 0; i < count; i++) {
saveObject(Array.get(value, i));
}
}
indent = oldIndent;
sink.write(indent + "</" + ARRAY_ELT + ">\n");
}
} // saveArray
/**
* This method saves the given object that must be an instance of
* <code>Map</code>. Each key and value component of the <code>Map</code>
* is recursively serialized/saved. Since <code>Map</code> objects
* can be shared, a unique id should be generated for this value so that
* it will be serialized only once.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param mapping the <code>Map</code> object itself
* @param id the generated unique identifier for the array object
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, the value
* component of a <code>Map</code> entry,
* or an entry in a <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
protected <K, V> void saveMap(Map<K, V> mapping,
Integer id,
String variable)
throws java.io.IOException
{
int count = mapping.size();
sink.write(indent + "<" + MAP_ELT
+ addVariableAttribute(variable)
+ addAttribute(ID_ATTR, id.toString())
+ addAttribute(SIZE_ATTR,
Integer.toString(count))
+ ">\n");
String oldIndent = indent;
indent += " ";
// Enumerate each key-value pair and recursively save the key and
// value objects. In this case, if the class of either is
// (effectively) String or primitive, saveObject will take care of it.
Iterator<Map.Entry<K, V>> pairs = mapping.entrySet().iterator();
while (pairs.hasNext()) {
Map.Entry<K, V> entry = pairs.next();
sink.write(indent + "<" + KEY_ELT + ">\n");
String currentIndent = indent;
indent += " ";
saveObject(entry.getKey());
indent = currentIndent;
sink.write(indent + "</" + KEY_ELT + ">\n");
saveObject(entry.getValue());
}
indent = oldIndent;
sink.write(indent + "</" + MAP_ELT + ">\n");
} // saveMap
/**
* This method saves the given object that must be an instance of
* <code>Collection</code>. Each element of the <code>Collection</code>
* is recursively serialized/saved. Since <code>Collection</code> objects
* can be shared, a unique id should be generated for this value so that
* it will be serialized only once.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param elts the <code>Collection</code> object itself
* @param id the generated unique identifier for the array object
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, the value
* component of a <code>Map</code> entry,
* or an entry in a <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
protected void saveCollection(Collection<?> elts,
Integer id,
String variable)
throws java.io.IOException
{
int count = elts.size();
sink.write(indent + "<" + COLLECTION_ELT
+ addVariableAttribute(variable)
+ addAttribute(ID_ATTR, id.toString())
+ addAttribute(SIZE_ATTR,
Integer.toString(count))
+ ">\n");
String oldIndent = indent;
indent += " ";
// Enumerate each element of the Collection and save recursively;
// if the class of either is (effectively) String or primitive,
// saveObject will take care of it.
Iterator<?> eltIt = elts.iterator();
while (eltIt.hasNext()) {
saveObject(eltIt.next());
}
indent = oldIndent;
sink.write(indent + "</" + COLLECTION_ELT + ">\n");
}
/**
* This method saves the given object. If the object is <code>null</null>,
* then the appropriate XML element is generated. Otherwise, a unique
* id is generated for the object; if one had already been assigned, then
* the appropriate XML element for an object reference is generated.
* If the object represents a primitive or String value, then the
* corresponding XML is generated. If the object if a member of an
* enumerated type, then save that way.
* <p>
* The XML format will be as described in ObjectPersist.java.
*
* @param value the object to be serialized/saved
* @param variable the name of the instance variable for this value in the
* containing object; may be <code>null</code> to indicate
* that value is an element in an array, the value
* component of a <code>Map</code> entry,
* or an entry in a <code>Collection</code>
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @throws UnexpectedException if no saver has been registered for the
* value's class
* @author mlh
*/
@SuppressWarnings("unchecked")
public <T> void saveObject(T value, String variable)
throws java.io.IOException
{
// If null, generate the null XML element
if (value == null) {
sink.write(indent
+ "<" + NULL_ELT
+ addVariableAttribute(variable)
+ "/>\n");
}
else {
Integer id = savedObjects.get(value);
// If previously saved, generate a reference element
if (id != null) {
saveReference(id, variable);
}
else {
Class<?> valueClass = value.getClass();
// If a primitive or String value, generate the corresponding
// element.
if (valueClass == Integer.class) {
saveInt(((Integer) value).intValue(), variable);
}
else if (valueClass == Long.class) {
saveLong(((Long) value).longValue(), variable);
}
else if (valueClass == Double.class) {
saveDouble(((Double) value).doubleValue(), variable);
}
else if (valueClass == Boolean.class) {
saveBoolean(((Boolean) value).booleanValue(), variable);
}
else if (valueClass == String.class) {
saveString((String) value, variable);
}
else if (valueClass == Character.class) {
saveChar(((Character) value).charValue(), variable);
}
else {
String className = valueClass.getName();
IDataSaver<T> saver =
(IDataSaver<T>) saverRegistry.getSaver(className);
// Check whether the value represents an enumerated value
if ((saver != null) && saver.isEnum()) {
saveEnum(className, saver, value, variable);
}
else {
// Assign a unique id to this value and register
id = new Integer(idGen++);
savedObjects.put(value, id);
// If an aggregate (array, Map, or Collection),
// save appropriately
if (valueClass.isArray()) {
saveArray(valueClass, value, id, variable);
}
else if (Map.class.isAssignableFrom(valueClass)) {
saveMap((Map<?, ?>) value, id, variable);
}
else if (Collection.class.isAssignableFrom(valueClass))
{
saveCollection((Collection<?>) value, id, variable);
}
else {
// If no saver has been registered, complain
if (saver == null) {
throw new IllegalStateException("no saver found for: " + className);
}
// Save object header, with current serialization
// format version.
String version =
Integer.toString(saver.getVersion());
sink.write(indent
+ "<" + OBJ_ELT
+ addVariableAttribute(variable)
+ addAttribute(ID_ATTR,
id.toString())
+ addAttribute(CLASS_ATTR,
className)
+ addAttribute(VERSION_ATTR,
version)
+ ">\n");
// Save any super class data that has registered
// savers.
String oldIndent = indent;
indent += " ";
saveAsSuper(value, valueClass);
// Save the data for this object corresponding
// only to this class.
saver.saveData(value, this);
indent = oldIndent;
// Close the XML element
sink.write(indent
+ "</" + OBJ_ELT + ">\n");
}
}
}
}
}
} // saveObject
/**
* Allow subclass to decide whether or not to record the given object
* to be serialized when all other objects have been saved.
* <p>
* Subclass should then override finish() to serialize the recorded
* objects.
* <p>
* This is useful for clipboard operations where not all of the child
* objects of a parent object should be serialized, and certainly not
* at the same point as the serialization of the parent object.
*
* @param value the object to be serialized/saved
* @return the filtered version of the given object value, if desired
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
public Object filterObject(Object value)
throws java.io.IOException
{
// If desired, subclass should override.
return value;
}
/**
* This method saves the data associated with the object's class'
* superclasses, as long as a saver has been registered for them. Once
* a superclass is encountered that has not registered saver, no further
* attempts are made, even if some ancestor superclass has registered a
* saver.
* <p>
* This method is recursive so that the data associated with more remote
* ancestor classes are saved before that associated with closer
* superclasses.
*
* @param obj the object being saved
* @param subclass the class whose superclass for which we are trying to
* save data
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
@SuppressWarnings("unchecked")
protected <T> void saveAsSuper(T obj, Class<?> subclass)
throws java.io.IOException
{
// There is no superclass of Object!
if (subclass != Object.class) {
Class<?> superclass = subclass.getSuperclass();
String superclassName = superclass.getName();
// Try to find a saver for the superclass
IDataSaver<T> saver =
(IDataSaver<T>) saverRegistry.getSaver(superclassName);
// Stop saving here if superclass doesn't require "persistence".
if (saver != null) {
// Otherwise, recursively try up the "tree".
saveAsSuper(obj, superclass);
// Regardless, save the data for this object that is associated
// with this superclass.
saveSuper(obj, superclassName, saver);
}
}
}
/**
* This method saves the data of the given object value that is associated
* with the specified superclass (<code>className</code>) of the value's
* object class. The given saver is the one registered for that
* superclass.
*
* @param value the object being saved
* @param className the name of the superclass whose data is being saved
* @param saver the saver object that controls how to serialize a value
* of the given type
* @throws java.io.IOException if the sink generates one during a call
* to <code>write</code>
* @author mlh
*/
protected <T> void saveSuper(T value,
String className,
IDataSaver<T> saver)
throws java.io.IOException
{
sink.write(indent
+ "<" + SUPER_ELT
+ addAttribute(CLASS_ATTR, className)
+ addAttribute(VERSION_ATTR,
Integer.toString(saver.getVersion()))
+ ">\n");
String oldIndent = indent;
indent += " ";
saver.saveData(value, this);
indent = oldIndent;
sink.write(indent + "</" + SUPER_ELT + ">\n");
}
}