/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de)
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////
package org.projectforge.database.xstream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.ClassUtils;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.projectforge.core.BaseDO;
import org.projectforge.core.IManualIndex;
import org.projectforge.database.HibernateUtils;
import org.projectforge.user.UserXmlPreferencesDO;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.ConverterLookup;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import de.micromata.hibernate.history.HistoryEntry;
import de.micromata.hibernate.history.delta.AssociationPropertyDelta;
import de.micromata.hibernate.history.delta.CollectionPropertyDelta;
import de.micromata.hibernate.history.delta.PropertyDelta;
import de.micromata.hibernate.history.delta.SimplePropertyDelta;
/**
* Registers all read objects and saves them in the configurable order to the data base.
* @author Kai Reinhard (k.reinhard@micromata.de)
*
*/
public class XStreamSavingConverter implements Converter
{
/** The logger */
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(XStreamSavingConverter.class);
private final ConverterLookup defaultConv;
private final Map<Class< ? >, List<Object>> allObjects = new HashMap<Class< ? >, List<Object>>();
private final Set<Class< ? >> writtenObjectTypes = new HashSet<Class< ? >>();
// Objekte dürfen nur einmal geschrieben werden, daher merken, was bereits gespeichert wurde
private final Set<Object> writtenObjects = new HashSet<Object>();
// Store the objects in the given order and all the other object types which are not listed here afterwards.
private final List<Class< ? >> orderOfSaving = new ArrayList<Class< ? >>();
// Ignore these objects from saving because the are saved implicit by their parent objects.
private final Set<Class< ? >> ignoreFromSaving = new HashSet<Class< ? >>();
// This map contains the mapping between the id's of the given xml stream and the new id's given by Hibernate. This is needed for writing
// the history entries with the new id's.
private final Map<String, Serializable> entityMapping = new HashMap<String, Serializable>();
private final List<HistoryEntry> historyEntries = new ArrayList<HistoryEntry>();
private final Map<String, Class< ? >> historyClassMapping = new HashMap<String, Class< ? >>();
private Session session;
public XStreamSavingConverter() throws HibernateException
{
final XStream xstream = new XStream();
defaultConv = xstream.getConverterLookup();
this.ignoreFromSaving.add(PropertyDelta.class);
this.ignoreFromSaving.add(SimplePropertyDelta.class);
this.ignoreFromSaving.add(AssociationPropertyDelta.class);
this.ignoreFromSaving.add(CollectionPropertyDelta.class);
}
public void setSession(final Session session)
{
this.session = session;
}
public Map<Class< ? >, List<Object>> getAllObjects()
{
return allObjects;
}
public List<HistoryEntry> getHistoryEntries()
{
return historyEntries;
}
@SuppressWarnings("rawtypes")
@Override
public boolean canConvert(final Class arg0)
{
return true;
}
public XStreamSavingConverter appendOrderedType(final Class< ? >... types)
{
if (types != null) {
for (final Class< ? > type : types) {
this.orderOfSaving.add(type);
}
}
return this;
}
public XStreamSavingConverter appendIgnoredObjects(final Class< ? >... types)
{
if (types != null) {
for (final Class< ? > type : types) {
this.ignoreFromSaving.add(type);
}
}
return this;
}
public void saveObjects()
{
for (final Class< ? > type : orderOfSaving) {
this.historyClassMapping.put(getClassname4History(type), type);
save(type);
}
for (final Map.Entry<Class< ? >, List<Object>> entry : allObjects.entrySet()) {
if (entry.getKey().equals(HistoryEntry.class) == true) {
continue;
}
final Class< ? > type = entry.getKey();
this.historyClassMapping.put(getClassname4History(type), type);
save(type);
}
for (final Class< ? > type : ignoreFromSaving) {
this.historyClassMapping.put(getClassname4History(type), type);
}
save(HistoryEntry.class);
}
/**
* Will be called directly before an object will be saved.
* @param obj
* @return id of the inserted objects if saved manually inside this method or null if the object has to be saved by save method (default).
*/
public Serializable onBeforeSave(final Session session, final Object obj)
{
if (obj instanceof HistoryEntry) {
final HistoryEntry entry = (HistoryEntry) obj;
final Integer origEntityId = entry.getEntityId();
final String entityClassname = entry.getClassName();
final Serializable newId = getNewId(entityClassname, origEntityId);
final List<PropertyDelta> delta = entry.getDelta();
Serializable id = null;
if (newId != null) {
// No public access, so try this:
invokeHistorySetter(entry, "setEntityId", Integer.class, newId);
} else {
log.error("Can't find mapping of old entity id. This results in a corrupted history: " + entry);
}
invokeHistorySetter(entry, "setDelta", List.class, null);
id = save(entry);
final List<PropertyDelta> list = new ArrayList<PropertyDelta>();
invokeHistorySetter(entry, "setDelta", List.class, list);
for (final PropertyDelta deltaEntry : delta) {
list.add(deltaEntry);
save(deltaEntry);
}
this.historyEntries.add(entry);
return id;
}
return null;
}
/**
* Does nothing at default.
* @param obj Please note: the id isn't yet set to this object!
* @param id The new id of the data-base.
*/
public void onAfterSave(final Object obj, final Serializable id)
{
}
protected Serializable save(final BaseDO< ? extends Serializable> obj,
final Collection< ? extends BaseDO< ? extends Serializable>> children)
{
final List<Serializable> oldIdList = beforeSave(children);
final Serializable id = save(obj);
afterSave(children, oldIdList);
return id;
}
/**
* Remove the id (pk) of every children and stores it to the returned list.
* @param children
* @return The list of (old) ids of the children.
*/
protected List<Serializable> beforeSave(final Collection< ? extends BaseDO< ? extends Serializable>> children)
{
if (children == null || children.size() == 0) {
return null;
}
final List<Serializable> idList = new ArrayList<Serializable>(children.size());
for (final BaseDO< ? > child : children) {
idList.add(child.getId());
child.setId(null);
}
return idList;
}
/**
* Registers all children with their old and new id.
* @param children
* @param oldIdList The returned list of beforeSave(...) method.
*/
protected void afterSave(final Collection< ? extends BaseDO< ? extends Serializable>> children, final List<Serializable> oldIdList)
{
if (oldIdList == null) {
return;
}
final Iterator<Serializable> oldIdListIterator = oldIdList.iterator();
final Iterator< ? extends BaseDO< ? >> childIterator = children.iterator();
while (oldIdListIterator.hasNext() == true) {
final BaseDO< ? > child = childIterator.next();
registerEntityMapping(child.getClass(), oldIdListIterator.next(), child.getId());
}
}
/**
* These methods are not public.
* @param name
* @param value
*/
private void invokeHistorySetter(final HistoryEntry entry, final String name, final Class< ? > parameterType, final Object value)
{
try {
final Method method = HistoryEntry.class.getDeclaredMethod(name, parameterType);
method.setAccessible(true);
method.invoke(entry, value);
} catch (final IllegalArgumentException ex) {
log.error("Can't modify id of history entry. This results in a corrupted history: " + entry);
log.fatal("Exception encountered " + ex, ex);
} catch (final IllegalAccessException ex) {
log.error("Can't modify id of history entry. This results in a corrupted history: " + entry);
log.fatal("Exception encountered " + ex, ex);
} catch (final InvocationTargetException ex) {
log.error("Can't modify id of history entry. This results in a corrupted history: " + entry);
log.fatal("Exception encountered " + ex, ex);
} catch (final SecurityException ex) {
log.error("Can't modify id of history entry. This results in a corrupted history: " + entry);
log.fatal("Exception encountered " + ex, ex);
} catch (final NoSuchMethodException ex) {
log.error("Can't modify id of history entry. This results in a corrupted history: " + entry);
log.fatal("Exception encountered " + ex, ex);
}
}
private void save(final Class< ? > type)
{
if (ignoreFromSaving.contains(type) == true || writtenObjectTypes.contains(type) == true) {
// Already written.
return;
}
writtenObjectTypes.add(type);
// Persistente Klasse?
if (HibernateUtils.isEntity(type) == false) {
return;
}
if (log.isDebugEnabled() == true) {
log.debug("Writing objects from type: " + type);
}
final List<Object> list = allObjects.get(type);
if (list == null) {
return;
}
for (final Object obj : list) {
if (obj == null || writtenObjects.contains(obj) == true) {
// Object null or already written. Skip this item.
continue;
}
if (session.contains(obj) == true) {
continue;
}
try {
if (log.isDebugEnabled()) {
log.debug("Try to write object " + obj);
}
Serializable id = onBeforeSave(session, obj);
if (id == null) {
id = save(obj);
}
onAfterSave(obj, id);
if (log.isDebugEnabled() == true) {
log.debug("wrote object " + obj + " under id " + id);
}
} catch (final HibernateException ex) {
log.fatal("Failed to write " + obj + " ex=" + ex, ex);
} catch (final NullPointerException ex) {
log.fatal("Failed to write " + obj + " ex=" + ex, ex);
}
}
}
/**
* Should return the id value of the imported xml object (the origin id of the data-base the dump is from).
* @param The object with the origin id.
* @return null if not overridden.
*/
protected Serializable getOriginalIdentifierValue(final Object obj)
{
return null;
}
protected Serializable save(final Object obj)
{
final Serializable oldId = getOriginalIdentifierValue(obj);
final Serializable id;
if (session.contains(obj) == false) {
if (obj instanceof BaseDO) {
if (obj instanceof IManualIndex == false) {
((BaseDO< ? >) obj).setId(null);
}
id = session.save(obj);
if (oldId != null) {
registerEntityMapping(obj.getClass(), oldId, id);
}
writtenObjects.add(obj);
} else if (obj instanceof HistoryEntry) {
// HistoryEntry
((HistoryEntry) obj).setId(null);
id = session.save(obj);
} else if (obj instanceof PropertyDelta) {
// PropertyDelta
((PropertyDelta) obj).setId(null);
id = session.save(obj);
} else if (obj instanceof UserXmlPreferencesDO) {
((UserXmlPreferencesDO) obj).setId(null);
id = session.save(obj);
} else {
log.warn("Unknown object: " + obj);
id = session.save(obj);
}
} else {
session.saveOrUpdate(obj);
id = ((BaseDO< ? >) obj).getId();
}
session.flush();
return id;
}
public Class< ? > getClassFromHistoryName(final String classname)
{
return this.historyClassMapping.get(classname);
}
private String getClassname4History(final Class< ? > cls)
{
return ClassUtils.getShortClassName(cls);
}
protected void registerEntityMapping(final Class< ? > entityClass, final Serializable oldId, final Serializable newId)
{
final Serializable registeredNewId = getNewId(entityClass, oldId);
if (registeredNewId != null && registeredNewId.equals(newId) == false) {
log.error("Oups, double entity mapping found for entity '"
+ entityClass
+ "' with old id="
+ oldId
+ " . New id "
+ newId
+ " ignored, using previous stored id "
+ registeredNewId
+ " instead.");
} else {
this.entityMapping.put(getClassname4History(entityClass) + oldId, newId);
}
}
public Integer getNewIdAsInteger(final Class<?> entityClass, final Integer oldId) {
final Serializable newId = getNewId(entityClass, oldId);
if (newId == null) {
log.error("Oups, can't find '" + entityClass + "' id '" + oldId + "'.");
return null;
} else if (newId instanceof Integer == false) {
log.error("Oups, can't get '" + entityClass + "' id '" + oldId + "' as integer: " + newId);
return null;
}
return (Integer)newId;
}
public Serializable getNewId(final Class< ? > entityClass, final Serializable oldId)
{
return getNewId(getClassname4History(entityClass), oldId);
}
protected Serializable getNewId(final String entityClassname, final Serializable oldId)
{
return this.entityMapping.get(entityClassname + oldId);
}
public void marshal(final Object arg0, final HierarchicalStreamWriter arg1, final MarshallingContext arg2)
{
defaultConv.lookupConverterForType(arg0.getClass()).marshal(arg0, arg1, arg2);
}
public Object unmarshal(final HierarchicalStreamReader arg0, final UnmarshallingContext arg1)
{
Object result;
Class< ? > targetType = null;
try {
targetType = arg1.getRequiredType();
result = defaultConv.lookupConverterForType(targetType).unmarshal(arg0, arg1);
} catch (final Exception ex) {
log.warn("Ignore unknown class or property " + targetType + " " + ex.getMessage());
return null;
}
try {
if (result != null) {
registerObject(result);
}
} catch (final HibernateException ex) {
log.fatal("Failed to write " + result + " ex=" + ex, ex);
} catch (final NullPointerException ex) {
log.fatal("Failed to write " + result + " ex=" + ex, ex);
}
return result;
}
private void registerObject(final Object obj)
{
if (obj == null) {
return;
}
if (HibernateUtils.isEntity(obj.getClass()) == false) {
return;
}
if (this.ignoreFromSaving.contains(obj.getClass()) == true) {
// Don't need this objects as "top level" objects in list. They're usually encapsulated.
return;
}
List<Object> list = this.allObjects.get(obj.getClass());
if (list == null) {
list = new ArrayList<Object>();
this.allObjects.put(obj.getClass(), list);
}
list.add(obj);
}
}