/*
* ==========================================================================*\
* | $Id: FlexibleFieldSetConverter.java,v 1.2 2011/02/18 20:52:39 stedwar2 Exp
* $
* |*-------------------------------------------------------------------------*|
* | Copyright (C) 2009-2010 Virginia Tech | | This file is part of the
* Student-Library. | | The Student-Library 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
* 3 of the | License, or (at your option) any later version. | | The
* Student-Library 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 the Student-Library; if not, see
* <http://www.gnu.org/licenses/>.
* \*==========================================================================
*/
package student.web.internal.converters;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.collections.TreeMapConverter;
import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.Mapper;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import student.web.AbstractPersistentMap;
import student.web.internal.LocalityService;
import student.web.internal.PersistentStorageManager.StoredObject;
import student.web.internal.Snapshot;
import student.web.internal.TemplateManager;
import student.web.internal.TemplateManager.Template;
import student.web.internal.TemplateManager.Template.Field;
// -------------------------------------------------------------------------
/**
* A custom XStream converter class that stores each object as a map from field
* names to field values.
*
* @author Stephen Edwards
* @author Last changed by $Author: mwoodsvt $
* @version $Revision: 1.7 $, $Date: 2011/06/21 14:57:28 $
*/
public class FlexibleFieldSetConverter extends ReflectionConverter
{
private TreeMapConverter mapConverter;
private Map<String, Object> context;
// Snapshot snapshots = new Snapshot();
// Snapshot oldSnapshots;
public class IllegalPersistException extends RuntimeException
{
/**
*
*/
private static final long serialVersionUID = -6748655411127633818L;
public IllegalPersistException( String message )
{
super( message );
}
}
// ----------------------------------------------------------
public FlexibleFieldSetConverter(
Mapper mapper,
ReflectionProvider rp,
Map<String, Object> context )
{
super( mapper, rp );
mapConverter = new TreeMapConverter( mapper );
this.context = context;
// clearSnapshots();
}
private Map<String, Object> generateUpdatedFieldSet(
Snapshot local,
Snapshot newest,
Object source,
Map<String, Object> fields )
{
if ( newest == null )
{
return fields;
}
Map<String, Object> finalFieldSet = new TreeMap<String, Object>();
Map<String, Object> localFieldSet = local.getFieldSetFromObject( source );
Map<String, Object> newestFieldSet = newest.getFieldSetFromId( Snapshot.lookupId( source,
false ) );
if ( localFieldSet == null && newestFieldSet == null )
return fields;
if ( localFieldSet != null && newestFieldSet != null )
{
Map<String, Object> localChangedFields = difference( localFieldSet,
fields );
Map<String, Object> newestChangedFields = difference( localFieldSet,
newestFieldSet );
newestChangedFields.putAll( localChangedFields );
finalFieldSet.putAll( localFieldSet );
finalFieldSet.putAll( newestChangedFields );
}
else if ( localFieldSet == null )
{
finalFieldSet.putAll( newestFieldSet );
}
else if ( newestFieldSet == null )
{
finalFieldSet.putAll( localFieldSet );
}
return finalFieldSet;
}
/**
* Compute the difference between two field sets. This is not the same as
* "set difference". Instead, it is really the "changes" map minus any
* entries that duplicate those in the "original". In other words, what
* entries in "changes" map to different values than the same entries in
* "original"?
*
* @param original
* The base field set to compare against
* @param changes
* The second (modified) field set
* @return A field set map that contains the values defined in changes that
* are either not present in or are different than in the original.
*/
public static Map<String, Object> difference(
Map<String, Object> original,
Map<String, Object> changes )
{
Map<String, Object> differences = new TreeMap<String, Object>();
for ( String key : changes.keySet() )
{
Object value = changes.get( key );
if ( ( value == null && original.get( key ) != null )
|| ( value != null && !( isPrimitiveValue( value ) && value.equals( original.get( key ) ) ) ) )
{
differences.put( key, value );
}
}
return differences;
}
private static boolean isPrimitiveValue( Object value )
{
return value instanceof String || value instanceof Number
|| value instanceof Boolean || value instanceof Character;
}
// ----------------------------------------------------------
public void marshal(
Object source,
HierarchicalStreamWriter writer,
MarshallingContext context )
{
if ( source instanceof AbstractPersistentMap )
{
throw new IllegalArgumentException( "You cannot store an object that contains a reference to a "
+ source.getClass().getSimpleName() );
}
writer.addAttribute( XMLConstants.FIELDSET_ATTRIBUTE, "true" );
Map<String, Object> fields = objectToFieldMap( source );
// BUG: The persistence store will attempt to persist an inner class.
// This is the same as storing an object that references the Application
// class
if ( fields.containsKey( "this$0" ) )
{
throw new IllegalArgumentException( "The class "
+ source.getClass().getName()
+ " cannot be persisted because the definition of this class is contained within the "
+ fields.get( "this$0" ).getClass().getName() + " class. Move "
+ source.getClass().getName()
+ " to its own JAVA file." );
}
checkFields( source.getClass().getName(), fields );
UUID id = Snapshot.lookupId( source, true );
writer.addAttribute( XMLConstants.ID_ATTRIBUTE, id.toString() );
Map<String, Object> updatedFieldSets = generateUpdatedFieldSet( Snapshot.getLocal(),
Snapshot.getNewest(),
source,
fields );
List<String> nulledKeys = new ArrayList<String>();
for ( String key : updatedFieldSets.keySet() )
{
if ( updatedFieldSets.get( key ) instanceof NullableClass )
{
NullableClass nClass = (NullableClass)updatedFieldSets.get( key );
nClass.writeHiddenClass( mapConverter, writer, context );
// updatedFieldSets.remove( key );
nulledKeys.add( key );
}
}
for ( String key : nulledKeys )
updatedFieldSets.remove( key );
restoreObjectFromFieldMap( source, updatedFieldSets );
Snapshot.getLocal().resolveObject( id, source, updatedFieldSets );
mapConverter.marshal( updatedFieldSets, writer, context );
}
private void checkFields( String source, Map<String, Object> fields )
{
TemplateManager tm = TemplateManager.getInstance();
if ( tm.isEnabled() )
{
Template classTemplate = tm.getTemplate( source );
if ( classTemplate == null )
{
return;
}
// Check for every required template field.
for ( Field templateField : classTemplate.getFields() )
{
if ( !fields.containsKey( templateField.getName() ) )
{
if ( !templateField.isNullable() )
{
throw new IllegalPersistException( "Your class must contain "
+ "the Field named \""
+ templateField.getName()
+ "\". "
+ "Please add this to your class definition." );
}
}
}
// Check all present fields to ensure they are nullable
for ( String classField : fields.keySet() )
{
Object value = fields.get( classField );
if ( value == null )
{
Field templateField = classTemplate.getField( classField );
if ( templateField != null )
{
if ( !templateField.isNullable() )
{
throw new IllegalPersistException( "You cannot store the field "
+ classField
+ " as null. Make sure the field contains some data" );
}
}
else
{
Field defaultField = classTemplate.getDefault();
if ( defaultField.isNullable() )
{
continue;
}
throw new IllegalPersistException( "Your implementation of "
+ source
+ "must contain a value for the field "
+ classField + "." );
}
}
}
}
}
private Object getInstance(UnmarshallingContext context)
{
Object result = null;
Class<?> clazz = context.getRequiredType();
try
{
Constructor<?> defaultConst = clazz
.getConstructor();
Object newResult = defaultConst.newInstance();
// restoreObjectFromFieldMap( newResult, fields );
result = newResult;
}
catch( Exception e)
{
result = reflectionProvider.newInstance( context.getRequiredType() );
}
return result;
// catch ( SecurityException e1 )
// {
// System.err.println( "You made your default constructor for "
// + result.getClass().getName()
// + " private. If you would like it to be used"
// + " by the persistence library please make it public" );
// }
// catch ( NoSuchMethodException e2 )
// {
// System.err.println( "The shared object \""
// + result.getClass().getName()
// + "\" you have loaded did not "
// + "contain all of the fields present in your original object. "
// + "We were also unable to find a method with the signature \"public "
// + "void initializeFields()\" or the "
// + "default constructor \"public "
// + result.getClass().getName()
// + "()\". If you have made assumptions "
// + "about the state of certain fields in your "
// + result.getClass().getName()
// + " Object please provide one of these methods to initialize the fields "
// + "not present in the datastore." );
// }
// catch ( Exception e3 )
// {
// System.err.println( "An exception occured when initializing your object with the default constructor." );
// }
}
// ----------------------------------------------------------
public Object unmarshal(
HierarchicalStreamReader reader,
UnmarshallingContext context )
{
// Object current = context.currentObject();
Object result = getInstance(context);
// Object result = null;
if ( top == true && key != null && cache != null)
{
// This is simply to prevent infinite recursion when you get an
// object out of the store that references this object. This stored
// object will be overwritten when everything is finished.
cache.put( key, new StoredObject(key,"ALIAS",result,Snapshot.getLocal(),Long.MAX_VALUE) );
top = false;
}
Map<String, Object> fields = null;
UUID id = null;
if ( reader.getAttribute( XMLConstants.FIELDSET_ATTRIBUTE ) != null )
{
String objId = reader.getAttribute( XMLConstants.ID_ATTRIBUTE );
id = UUID.fromString( objId );
@SuppressWarnings("unchecked")
Map<String, Object> realFields = (Map<String, Object>)mapConverter.unmarshal( reader,
context );
fields = realFields;
}
else
{
result = super.unmarshal( reader, context );
if ( result instanceof TreeMap
&& !TreeMap.class.isAssignableFrom( context.getRequiredType() ) )
{
@SuppressWarnings("unchecked")
Map<String, Object> realFields = (Map<String, Object>)result;
fields = realFields;
}
}
if ( fields != null )
{
try
{
Method initFieldsMethod = result.getClass()
.getMethod( "initializeFields", (Class<?>)null );
initFieldsMethod.invoke( result, (Object[])null );
}
catch(Exception e)
{
//its ok if this doesnt exist
}
// result = reflectionProvider.newInstance( context.getRequiredType() );
restoreObjectFromFieldMap( result, fields ); /*
* && pjProvider
* != null
*/
// {
// try
// {
// Method initFieldsMethod = result.getClass()
// .getMethod( "initializeFields", (Class<?>)null );
// initFieldsMethod.invoke( result, (Object[])null );
// }
// catch ( Exception e )
// {
// try
// {
// Constructor<?> defaultConst = result.getClass()
// .getConstructor();
// Object newResult = defaultConst.newInstance();
// restoreObjectFromFieldMap( newResult, fields );
// result = newResult;
//
// }
// catch ( SecurityException e1 )
// {
// System.err.println( "You made your default constructor for "
// + result.getClass().getName()
// + " private. If you would like it to be used"
// + " by the persistence library please make it public" );
// }
// catch ( NoSuchMethodException e2 )
// {
// System.err.println( "The shared object \""
// + result.getClass().getName()
// + "\" you have loaded did not "
// + "contain all of the fields present in your original object. "
// + "We were also unable to find a method with the signature \"public "
// + "void initializeFields()\" or the "
// + "default constructor \"public "
// + result.getClass().getName()
// + "()\". If you have made assumptions "
// + "about the state of certain fields in your "
// + result.getClass().getName()
// + " Object please provide one of these methods to initialize the fields "
// + "not present in the datastore." );
// }
// catch ( Exception e3 )
// {
// System.err.println( "An exception occured when initializing your object with the default constructor." );
// }
//
// }
// }
if ( result != null )
{
Snapshot.getLocal().resolveObject( id, result, fields );
}
}
return result;
}
// ----------------------------------------------------------
/**
* Convert an object to a map of field name/value pairs.
*
* @param object
* The object to convert
* @return The object's field values in map form
*/
private Map<String, Object> objectToFieldMap( Object object )
{
final TreeMap<String, Object> result = new TreeMap<String, Object>();
reflectionProvider.visitSerializableFields( object,
new ReflectionProvider.Visitor()
{
@SuppressWarnings("rawtypes")
public void visit(
String fieldName,
Class type,
Class definedIn,
Object value )
{
result.put( fieldName, value );
}
} );
return result;
}
private static class BooleanWrapper
{
boolean value;
}
// ----------------------------------------------------------
/**
* Convert an object to a map of field name/value pairs.
*
* @param object
* The object to convert
* @param fields
* The field values to restore from
* @return True if all fields in the object were present in the field set
*/
private boolean restoreObjectFromFieldMap(
Object object,
final Map<String, Object> fields )
{
final Object result = object;
final BooleanWrapper allFound = new BooleanWrapper();
allFound.value = true;
reflectionProvider.visitSerializableFields( result,
new ReflectionProvider.Visitor()
{
@SuppressWarnings("rawtypes")
public void visit(
String fieldName,
Class type,
Class definedIn,
Object value )
{
Object toLoad = fields.get( fieldName );
if ( toLoad != null )
{
reflectionProvider.writeField( result,
fieldName,
fields.get( fieldName ),
definedIn );
}
else
{
allFound.value = false;
}
}
} );
return allFound.value;
}
// ----------------------------------------------------------
@SuppressWarnings("rawtypes")
public boolean canConvert( Class type )
{
return type != null;
}
private String key = null;
private Map<String,StoredObject> cache = null;
private boolean top = true;
public void setContext( String key, Map<String, StoredObject> cache )
{
this.top = true;
this.key = key;
this.cache = cache;
}
}