///////////////////////////////////////////////////////////////////////////// // // 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; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import javax.persistence.Transient; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.ClassUtils; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.Validate; import org.apache.log4j.Logger; import org.hibernate.EmptyInterceptor; import org.hibernate.FlushMode; import org.hibernate.Hibernate; import org.hibernate.LockOptions; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.projectforge.access.AccessEntryDO; import org.projectforge.access.GroupTaskAccessDO; import org.projectforge.common.BeanHelper; import org.projectforge.common.XStreamHelper; import org.projectforge.core.AbstractBaseDO; import org.projectforge.core.ConfigurationDO; import org.projectforge.database.xstream.HibernateXmlConverter; import org.projectforge.database.xstream.XStreamSavingConverter; import org.projectforge.fibu.AbstractRechnungDO; import org.projectforge.fibu.AbstractRechnungsPositionDO; import org.projectforge.fibu.AuftragDO; import org.projectforge.fibu.AuftragsPositionDO; import org.projectforge.fibu.EingangsrechnungDO; import org.projectforge.fibu.EingangsrechnungsPositionDO; import org.projectforge.fibu.EmployeeSalaryDO; import org.projectforge.fibu.KontoDO; import org.projectforge.fibu.KundeDO; import org.projectforge.fibu.ProjektDO; import org.projectforge.fibu.RechnungDO; import org.projectforge.fibu.RechnungsPositionDO; import org.projectforge.fibu.kost.Kost1DO; import org.projectforge.fibu.kost.Kost2ArtDO; import org.projectforge.fibu.kost.Kost2DO; import org.projectforge.fibu.kost.KostZuweisungDO; import org.projectforge.plugins.core.AbstractPlugin; import org.projectforge.plugins.core.PluginsRegistry; import org.projectforge.task.TaskDO; import org.projectforge.user.GroupDO; import org.projectforge.user.PFUserDO; import org.projectforge.user.UserPrefDO; import org.projectforge.user.UserPrefEntryDO; import org.projectforge.user.UserRightDO; import org.springframework.core.io.ClassPathResource; import org.springframework.orm.hibernate3.HibernateTemplate; import com.thoughtworks.xstream.XStream; import de.micromata.hibernate.history.HistoryEntry; /** * Dumps and restores the data-base. * @author Kai Reinhard (k.reinhard@micromata.de) * */ public class XmlDump { private static final Logger log = Logger.getLogger(XmlDump.class); private static final String XML_DUMP_FILENAME = System.getProperty("user.home") + "/tmp/database-dump.xml.gz"; private HibernateTemplate hibernate; private final List<XmlDumpHook> xmlDumpHooks = new LinkedList<XmlDumpHook>(); /** * These classes are stored automatically because they're dependent. */ private final Class< ? >[] embeddedClasses = new Class< ? >[] { UserRightDO.class, AuftragsPositionDO.class, EingangsrechnungsPositionDO.class, RechnungsPositionDO.class}; public HibernateTemplate getHibernate() { Validate.notNull(hibernate); return hibernate; } public void setHibernate(final HibernateTemplate hibernate) { this.hibernate = hibernate; } public void registerHook(final XmlDumpHook xmlDumpHook) { for (final XmlDumpHook hook : xmlDumpHooks) { if (hook.getClass().equals(xmlDumpHook.getClass()) == true) { log.error("Can't register XmlDumpHook twice: " + xmlDumpHook); return; } } xmlDumpHooks.add(xmlDumpHook); } /** * @return Only for test cases. */ public XStreamSavingConverter restoreDatabase() { try { return restoreDatabase(new InputStreamReader(new FileInputStream(XML_DUMP_FILENAME), "utf-8")); } catch (final UnsupportedEncodingException ex) { log.error(ex.getMessage(), ex); throw new RuntimeException(ex); } catch (final FileNotFoundException ex) { log.error(ex.getMessage(), ex); throw new RuntimeException(ex); } } /** * @param reader * @return Only for test cases. */ public XStreamSavingConverter restoreDatabase(final Reader reader) { final List<AbstractPlugin> plugins = PluginsRegistry.instance().getPlugins(); final XStreamSavingConverter xstreamSavingConverter = new XStreamSavingConverter() { @Override protected Serializable getOriginalIdentifierValue(final Object obj) { return HibernateUtils.getIdentifier(obj); } @Override public Serializable onBeforeSave(final Session session, final Object obj) { log.info("Object " + obj); if (obj instanceof PFUserDO) { final PFUserDO user = (PFUserDO) obj; return save(user, user.getRights()); } else if (obj instanceof AbstractRechnungDO< ? >) { final AbstractRechnungDO< ? extends AbstractRechnungsPositionDO> rechnung = (AbstractRechnungDO< ? >) obj; final List< ? extends AbstractRechnungsPositionDO> positions = rechnung.getPositionen(); final KontoDO konto = rechnung.getKonto(); if (konto != null) { save(konto); rechnung.setKonto(null); } rechnung.setPositionen(null); // Need to nullable positions first (otherwise insert fails). final Serializable id = save(rechnung); if (konto != null) { rechnung.setKonto(konto); } if (positions != null) { for (final AbstractRechnungsPositionDO pos : positions) { if (pos.getKostZuweisungen() != null) { final List<KostZuweisungDO> zuweisungen = pos.getKostZuweisungen(); pos.setKostZuweisungen(null); // Need to nullable first (otherwise insert fails). save(pos); if (pos instanceof RechnungsPositionDO) { ((RechnungDO) rechnung).addPosition((RechnungsPositionDO) pos); } else { ((EingangsrechnungDO) rechnung).addPosition((EingangsrechnungsPositionDO) pos); } if (zuweisungen != null) { for (final KostZuweisungDO zuweisung : zuweisungen) { pos.addKostZuweisung(zuweisung); save(zuweisung); } } } } } return id; } else if (obj instanceof AuftragDO) { final AuftragDO auftrag = (AuftragDO) obj; return save(auftrag, auftrag.getPositionen()); } if (plugins != null) { for (final AbstractPlugin plugin : plugins) { try { plugin.onBeforeRestore(this, obj); } catch (final Exception ex) { log.error("Error in Plugin while restoring object: " + ex.getMessage(), ex); } } } for (final XmlDumpHook xmlDumpHook : xmlDumpHooks) { try { xmlDumpHook.onBeforeRestore(this, obj); } catch (final Exception ex) { log.error("Error in XmlDumpHook while restoring object: " + ex.getMessage(), ex); } } return super.onBeforeSave(session, obj); } /** * @see org.projectforge.database.xstream.XStreamSavingConverter#onAfterSave(java.lang.Object, java.io.Serializable) */ @Override public void onAfterSave(final Object obj, final Serializable id) { if (plugins != null) { for (final AbstractPlugin plugin : plugins) { plugin.onAfterRestore(this, obj, id); } } } }; // UserRightDO is inserted on cascade while inserting PFUserDO. xstreamSavingConverter.appendIgnoredObjects(embeddedClasses); xstreamSavingConverter.appendOrderedType(PFUserDO.class, GroupDO.class, TaskDO.class, KundeDO.class, ProjektDO.class, Kost1DO.class, Kost2ArtDO.class, Kost2DO.class, AuftragDO.class, // RechnungDO.class, EingangsrechnungDO.class, EmployeeSalaryDO.class, KostZuweisungDO.class,// UserPrefEntryDO.class, UserPrefDO.class, // AccessEntryDO.class, GroupTaskAccessDO.class, ConfigurationDO.class); if (plugins != null) { for (final AbstractPlugin plugin : plugins) { xstreamSavingConverter.appendOrderedType(plugin.getPersistentEntities()); } } Session session = null; try { final SessionFactory sessionFactory = hibernate.getSessionFactory(); session = sessionFactory.openSession(EmptyInterceptor.INSTANCE); session.setFlushMode(FlushMode.AUTO); final XStream xstream = XStreamHelper.createXStream(); xstream.setMode(XStream.ID_REFERENCES); xstreamSavingConverter.setSession(session); xstream.registerConverter(xstreamSavingConverter, 10); xstream.registerConverter(new UserRightIdSingleValueConverter(), 20); xstream.registerConverter(new UserPrefAreaSingleValueConverter(), 19); // alle Objekte Laden und speichern xstream.fromXML(reader); xstreamSavingConverter.saveObjects(); } catch (final Exception ex) { log.error(ex.getMessage(), ex); throw new RuntimeException(ex); } finally { IOUtils.closeQuietly(reader); if (session != null) { session.close(); } } return xstreamSavingConverter; } /** * @return Only for test cases. */ public XStreamSavingConverter restoreDatabaseFromClasspathResource(final String path, final String encoding) { final ClassPathResource cpres = new ClassPathResource(path); Reader reader; try { InputStream in; if (path.endsWith(".gz") == true) { in = new GZIPInputStream(cpres.getInputStream()); } else { in = cpres.getInputStream(); } reader = new InputStreamReader(in, encoding); } catch (final IOException ex) { log.error(ex.getMessage(), ex); throw new RuntimeException(ex); } return restoreDatabase(reader); } public void dumpDatabase() { dumpDatabase(XML_DUMP_FILENAME, "utf-8"); } /** * * @param filename virtual filename: If the filename suffix is "gz" then the dump will be compressed. * @param out */ public void dumpDatabase(final String filename, final OutputStream out) { final HibernateXmlConverter converter = new HibernateXmlConverter() { @Override protected void init(final XStream xstream) { xstream.omitField(AbstractBaseDO.class, "minorChange"); xstream.omitField(AbstractBaseDO.class, "selected"); xstream.registerConverter(new UserRightIdSingleValueConverter(), 20); xstream.registerConverter(new UserPrefAreaSingleValueConverter(), 19); } }; converter.setHibernate(hibernate); converter.appendIgnoredTopLevelObjects(embeddedClasses); Writer writer = null; GZIPOutputStream gzipOut = null; try { if (filename.endsWith(".gz") == true) { gzipOut = new GZIPOutputStream(out); writer = new OutputStreamWriter(gzipOut, "utf-8"); } else { writer = new OutputStreamWriter(out, "utf-8"); } converter.dumpDatabaseToXml(writer, true); // history=false, preserveIds=true } catch (final IOException ex) { log.error(ex.getMessage(), ex); } finally { IOUtils.closeQuietly(gzipOut); IOUtils.closeQuietly(writer); } } public void dumpDatabase(final String path, final String encoding) { OutputStream out = null; try { out = new FileOutputStream(path); dumpDatabase(path, out); } catch (final IOException ex) { log.error(ex.getMessage(), ex); } finally { IOUtils.closeQuietly(out); } } /** * Verify the imported dump. * @return Number of checked objects. This number is negative if any error occurs (at least one object wasn't imported successfully). */ public int verifyDump(final XStreamSavingConverter xstreamSavingConverter) { final SessionFactory sessionFactory = hibernate.getSessionFactory(); Session session = null; boolean hasError = false; try { session = sessionFactory.openSession(EmptyInterceptor.INSTANCE); session.setDefaultReadOnly(true); int counter = 0; for (final Map.Entry<Class< ? >, List<Object>> entry : xstreamSavingConverter.getAllObjects().entrySet()) { final List<Object> objects = entry.getValue(); final Class< ? > entityClass = entry.getKey(); if (objects == null) { continue; } for (final Object obj : objects) { if (HibernateUtils.isEntity(obj.getClass()) == false) { continue; } final Serializable id = HibernateUtils.getIdentifier(obj); if (id == null) { // Can't compare this object without identifier. continue; } // log.info("Testing object: " + obj); final Object databaseObject = session.get(entityClass, id, LockOptions.READ); Hibernate.initialize(databaseObject); final boolean equals = equals(obj, databaseObject, true); if (equals == false) { log.error("Object not sucessfully imported! xml object=[" + obj + "], data base=[" + databaseObject + "]"); hasError = true; } ++counter; } } for (final HistoryEntry historyEntry : xstreamSavingConverter.getHistoryEntries()) { final Class< ? > type = xstreamSavingConverter.getClassFromHistoryName(historyEntry.getClassName()); final Object o = type != null ? session.get(type, historyEntry.getEntityId()) : null; if (o == null) { log.warn("A corrupted history entry found (entity of class '" + historyEntry.getClassName() + "' with id " + historyEntry.getEntityId() + " not found: " + historyEntry + ". This doesn't affect the functioning of ProjectForge, this may result in orphaned history entries."); hasError = true; } ++counter; } if (hasError == true) { log.fatal("*********** A inconsistency in the import was found! This may result in a data loss or corrupted data! Please retry the import. " + counter + " entries checked."); return -counter; } log.info("Data-base import successfully verified: " + counter + " entries checked."); return counter; } finally { if (session != null) { session.close(); } } } /** * @param o1 * @param o2 * @param logDifference If true than the difference is logged. * @return True if the given objects are equal. */ private boolean equals(final Object o1, final Object o2, final boolean logDifference) { if (o1 == null) { final boolean equals = (o2 == null); if (equals == false && logDifference == true) { log.error("Value 1 is null and value 2 is " + o2); } return equals; } else if (o2 == null) { if (logDifference == true) { log.error("Value 2 is null and value 1 is " + o1); } return false; } final Class< ? > cls1 = o1.getClass(); final Field[] fields = cls1.getDeclaredFields(); AccessibleObject.setAccessible(fields, true); for (final Field field : fields) { if (accept(field) == false) { continue; } try { final Object fieldValue1 = getValue(o1, o2, field); final Object fieldValue2 = getValue(o2, o1, field); if (field.getType().isPrimitive() == true) { if (ObjectUtils.equals(fieldValue2, fieldValue1) == false) { if (logDifference == true) { log.error("Field '" + field.getName() + "': value 1 '" + fieldValue1 + "' is different from value 2 '" + fieldValue2 + "'."); } return false; } continue; } else if (fieldValue1 == null) { if (fieldValue2 != null) { if (fieldValue2 instanceof Collection< ? >) { if (CollectionUtils.isEmpty((Collection< ? >) fieldValue2) == true) { // null is equals to empty collection in this case. return true; } } if (logDifference == true) { log.error("Field '" + field.getName() + "': value 1 '" + fieldValue1 + "' is different from value 2 '" + fieldValue2 + "'."); } return false; } } else if (fieldValue2 == null) { if (fieldValue1 != null) { if (logDifference == true) { log.error("Field '" + field.getName() + "': value 1 '" + fieldValue1 + "' is different from value 2 '" + fieldValue2 + "'."); } return false; } } else if (fieldValue1 instanceof Collection< ? >) { final Collection< ? > col1 = (Collection< ? >) fieldValue1; final Collection< ? > col2 = (Collection< ? >) fieldValue2; if (col1.size() != col2.size()) { if (logDifference == true) { log.error("Field '" + field.getName() + "': colection's size '" + col1.size() + "' is different from collection's size '" + col2.size() + "'."); } return false; } if (equals(field, col1, col2, logDifference) == false || equals(field, col2, col1, logDifference) == false) { return false; } } else if (HibernateUtils.isEntity(fieldValue1.getClass()) == true) { if (fieldValue2 == null || ObjectUtils.equals(HibernateUtils.getIdentifier(fieldValue1), HibernateUtils.getIdentifier(fieldValue2)) == false) { if (logDifference == true) { log.error("Field '" + field.getName() + "': Hibernate object id '" + HibernateUtils.getIdentifier(fieldValue1) + "' is different from id '" + HibernateUtils.getIdentifier(fieldValue2) + "'."); } return false; } } else if (fieldValue1 instanceof BigDecimal) { if (fieldValue2 == null || ((BigDecimal) fieldValue1).compareTo((BigDecimal) fieldValue2) != 0) { if (logDifference == true) { log.error("Field '" + field.getName() + "': value 1 '" + fieldValue1 + "' is different from value 2 '" + fieldValue2 + "'."); } return false; } } else if (fieldValue1.getClass().isArray() == true) { if (ArrayUtils.isEquals(fieldValue1, fieldValue2) == false) { if (logDifference == true) { log.error("Field '" + field.getName() + "': value 1 '" + fieldValue1 + "' is different from value 2 '" + fieldValue2 + "'."); } return false; } } else if (ObjectUtils.equals(fieldValue2, fieldValue1) == false) { if (logDifference == true) { log.error("Field '" + field.getName() + "': value 1 '" + fieldValue1 + "' is different from value 2 '" + fieldValue2 + "'."); } return false; } } catch (final IllegalAccessException ex) { throw new InternalError("Unexpected IllegalAccessException: " + ex.getMessage()); } } return true; } /** * Tests if every entry of col1 is found as equals entry in col2. You need to call this method twice with swapped params for being sure of * equality! * @param col1 * @param col2 * @return */ private boolean equals(final Field field, final Collection< ? > col1, final Collection< ? > col2, final boolean logDifference) { for (final Object colVal1 : col1) { boolean equals = false; for (final Object colVal2 : col2) { if (equals(colVal1, colVal2, false) == true) { equals = true; // Equal object found. break; } } if (equals == false) { if (logDifference == true) { log.error("Field '" + field.getName() + "': value '" + colVal1 + "' not found in other collection."); } return false; } } return true; } /** * @param obj * @param compareObj Only need for @Transient (because Javassist proxy doesn't have this annotion). * @param field * @return * @throws IllegalArgumentException * @throws IllegalAccessException */ private Object getValue(final Object obj, final Object compareObj, final Field field) throws IllegalArgumentException, IllegalAccessException { Object val = null; final Method getter = BeanHelper.determineGetter(obj.getClass(), field.getName()); final Method getter2 = BeanHelper.determineGetter(compareObj.getClass(), field.getName()); if (getter != null && getter.isAnnotationPresent(Transient.class) == false && getter2 != null && getter2.isAnnotationPresent(Transient.class) == false) { val = BeanHelper.invoke(obj, getter); } if (val == null) { val = field.get(obj); } return val; } /** * @param field * @return true, if the given field should be compared. */ protected boolean accept(final Field field) { if (field.getName().indexOf(ClassUtils.INNER_CLASS_SEPARATOR_CHAR) != -1) { // Reject field from inner class. return false; } if (field.getName().equals("handler") == true) { // Handler of Javassist proxy should be ignored. return false; } if (Modifier.isTransient(field.getModifiers()) == true) { // transients. return false; } if (Modifier.isStatic(field.getModifiers()) == true) { // transients. return false; } return true; } }