///////////////////////////////////////////////////////////////////////////// // // 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.core; import java.io.Serializable; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.TreeSet; import javax.persistence.Basic; import javax.persistence.Column; import javax.persistence.MappedSuperclass; import javax.persistence.Transient; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.ClassUtils; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.hibernate.collection.PersistentSet; import org.projectforge.calendar.DayHolder; import org.projectforge.common.ReflectionToString; import org.projectforge.database.HibernateUtils; /** * * @author Kai Reinhard (k.reinhard@micromata.de) * */ @MappedSuperclass public abstract class AbstractBaseDO<I extends Serializable> implements ExtendedBaseDO<I>, Serializable { private static final long serialVersionUID = -2225460450662176301L; private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(AbstractBaseDO.class); @PropertyInfo(i18nKey = "created") private Date created; @PropertyInfo(i18nKey = "modified") private Date lastUpdate; @PropertyInfo(i18nKey = "deleted") private boolean deleted; private boolean minorChange = false; private transient Map<String, Object> attributeMap; /** * If any re-calculations have to be done before displaying, indexing etc. This method have an implementation if a data object has * transient fields which are calculated by other fields. This default implementation does nothing. */ public void recalculate() { } @Basic public boolean isDeleted() { return deleted; } public void setDeleted(final boolean deleted) { this.deleted = deleted; } @Basic public Date getCreated() { return created; } public void setCreated(final Date created) { this.created = created; } public void setCreated() { this.created = new Date(); } /** * * Last update will be modified automatically for every update of the database object. * @return */ @Basic @Column(name = "last_update") public Date getLastUpdate() { return lastUpdate; } public void setLastUpdate(final Date lastUpdate) { this.lastUpdate = lastUpdate; } public void setLastUpdate() { this.lastUpdate = new Date(); } /** * Default value is false. * @see org.projectforge.core.BaseDO#isMinorChange() */ @Transient public boolean isMinorChange() { return minorChange; } public void setMinorChange(final boolean value) { this.minorChange = value; } public Object getAttribute(final String key) { if (attributeMap == null) { return null; } return attributeMap.get(key); } public void setAttribute(final String key, final Object value) { synchronized (attributeMap) { if (attributeMap == null) { attributeMap = new HashMap<String, Object>(); } } attributeMap.put(key, value); } /** * Returns string containing all fields (except the password, via ReflectionToStringBuilder). * @return */ @Override public String toString() { return ReflectionToString.asString(this); } /** * Copies all values from the given src object excluding the values created and lastUpdate. Do not overwrite created and lastUpdate from * the original database object. * @param src * @param ignoreFields Does not copy these properties (by field name). * @return true, if any modifications are detected, otherwise false; */ public ModificationStatus copyValuesFrom(final BaseDO< ? extends Serializable> src, final String... ignoreFields) { return copyValues(src, this, ignoreFields); } /** * Copies all values from the given src object excluding the values created and lastUpdate. Do not overwrite created and lastUpdate from * the original database object. * @param src * @param dest * @param ignoreFields Does not copy these properties (by field name). * @return true, if any modifications are detected, otherwise false; */ @SuppressWarnings("unchecked") public static ModificationStatus copyValues(final BaseDO src, final BaseDO dest, final String... ignoreFields) { if (ClassUtils.isAssignable(src.getClass(), dest.getClass()) == false) { throw new RuntimeException("Try to copyValues from different BaseDO classes: this from type " + dest.getClass().getName() + " and src from type" + src.getClass().getName() + "!"); } if (src.getId() != null && (ignoreFields == null || ArrayUtils.contains(ignoreFields, "id") == false)) { dest.setId(src.getId()); } return copyDeclaredFields(src.getClass(), src, dest, ignoreFields); } /** * * @param srcClazz * @param src * @param dest * @param ignoreFields * @return true, if any modifications are detected, otherwise false; */ @SuppressWarnings("unchecked") private static ModificationStatus copyDeclaredFields(final Class< ? > srcClazz, final BaseDO< ? > src, final BaseDO< ? > dest, final String... ignoreFields) { final Field[] fields = srcClazz.getDeclaredFields(); AccessibleObject.setAccessible(fields, true); ModificationStatus modificationStatus = null; for (final Field field : fields) { final String fieldName = field.getName(); if ((ignoreFields != null && ArrayUtils.contains(ignoreFields, fieldName) == true) || accept(field) == false) { continue; } try { final Object srcFieldValue = field.get(src); final Object destFieldValue = field.get(dest); if (field.getType().isPrimitive() == true) { if (ObjectUtils.equals(destFieldValue, srcFieldValue) == false) { field.set(dest, srcFieldValue); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } continue; } else if (srcFieldValue == null) { if (field.getType() == String.class) { if (StringUtils.isNotEmpty((String) destFieldValue) == true) { field.set(dest, null); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } } else if (destFieldValue != null) { field.set(dest, null); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } else { // dest was already null } } else if (srcFieldValue instanceof Collection) { Collection<Object> destColl = (Collection<Object>) destFieldValue; final Collection<Object> srcColl = (Collection<Object>) srcFieldValue; final Collection<Object> toRemove = new ArrayList<Object>(); if (srcColl != null && destColl == null) { if (srcColl instanceof TreeSet) { destColl = new TreeSet<Object>(); } else if (srcColl instanceof HashSet) { destColl = new HashSet<Object>(); } else if (srcColl instanceof List) { destColl = new ArrayList<Object>(); } else if (srcColl instanceof PersistentSet) { destColl = new HashSet<Object>(); } else { log.error("Unsupported collection type: " + srcColl.getClass().getName()); } field.set(dest, destColl); } for (final Object o : destColl) { if (srcColl.contains(o) == false) { toRemove.add(o); } } for (final Object o : toRemove) { if (log.isDebugEnabled() == true) { log.debug("Removing collection entry: " + o); } destColl.remove(o); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } for (final Object srcEntry : srcColl) { if (destColl.contains(srcEntry) == false) { if (log.isDebugEnabled() == true) { log.debug("Adding new collection entry: " + srcEntry); } destColl.add(srcEntry); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } else if (srcEntry instanceof BaseDO) { final PFPersistancyBehavior behavior = field.getAnnotation(PFPersistancyBehavior.class); if (behavior != null && behavior.autoUpdateCollectionEntries() == true) { BaseDO< ? > destEntry = null; for (final Object entry : destColl) { if (entry.equals(srcEntry) == true) { destEntry = (BaseDO< ? >) entry; break; } } Validate.notNull(destEntry); final ModificationStatus st = destEntry.copyValuesFrom((BaseDO< ? >) srcEntry); modificationStatus = getModificationStatus(modificationStatus, st); } } } } else if (srcFieldValue instanceof BaseDO) { final Serializable srcFieldValueId = HibernateUtils.getIdentifier((BaseDO< ? >) srcFieldValue); if (srcFieldValueId != null) { if (destFieldValue == null || ObjectUtils.equals(srcFieldValueId, ((BaseDO< ? >) destFieldValue).getId()) == false) { field.set(dest, srcFieldValue); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } } else { log.error("Can't get id though can't copy the BaseDO (see error message above about HHH-3502)."); } } else if (srcFieldValue instanceof java.sql.Date) { if (destFieldValue == null) { field.set(dest, srcFieldValue); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } else { final DayHolder srcDay = new DayHolder((Date) srcFieldValue); final DayHolder destDay = new DayHolder((Date) destFieldValue); if (srcDay.isSameDay(destDay) == false) { field.set(dest, srcDay.getSQLDate()); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } } } else if (srcFieldValue instanceof Date) { if (destFieldValue == null || ((Date) srcFieldValue).getTime() != ((Date) destFieldValue).getTime()) { field.set(dest, srcFieldValue); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } } else if (srcFieldValue instanceof BigDecimal) { if (destFieldValue == null || ((BigDecimal) srcFieldValue).compareTo((BigDecimal) destFieldValue) != 0) { field.set(dest, srcFieldValue); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } } else if (ObjectUtils.equals(destFieldValue, srcFieldValue) == false) { field.set(dest, srcFieldValue); modificationStatus = getModificationStatus(modificationStatus, src, fieldName); } } catch (final IllegalAccessException ex) { throw new InternalError("Unexpected IllegalAccessException: " + ex.getMessage()); } } final Class< ? > superClazz = srcClazz.getSuperclass(); if (superClazz != null) { final ModificationStatus st = copyDeclaredFields(superClazz, src, dest, ignoreFields); modificationStatus = getModificationStatus(modificationStatus, st); } return modificationStatus; } protected static ModificationStatus getModificationStatus(final ModificationStatus currentStatus, final BaseDO< ? > src, final String modifiedField) { if (currentStatus == ModificationStatus.MAJOR || src instanceof AbstractHistorizableBaseDO == false || ((AbstractHistorizableBaseDO< ? >) src).isNonHistorizableAttribute(modifiedField) == false) { return ModificationStatus.MAJOR; } return ModificationStatus.MINOR; } public static ModificationStatus getModificationStatus(final ModificationStatus currentStatus, final ModificationStatus status) { if (currentStatus == ModificationStatus.MAJOR || status == ModificationStatus.MAJOR) { return ModificationStatus.MAJOR; } if (currentStatus == ModificationStatus.MINOR || status == ModificationStatus.MINOR) { return ModificationStatus.MINOR; } return ModificationStatus.NONE; } /** * Returns whether or not to append the given <code>Field</code>. * <ul> * <li>Ignore transient fields * <li>Ignore static fields * <li>Ignore inner class fields</li> * </ul> * * @param field The Field to test. * @return Whether or not to consider the given <code>Field</code>. */ protected static boolean accept(final Field field) { if (field.getName().indexOf(ClassUtils.INNER_CLASS_SEPARATOR_CHAR) != -1) { // Reject field from inner class. return false; } if (Modifier.isTransient(field.getModifiers()) == true) { // transients. return false; } if (Modifier.isStatic(field.getModifiers()) == true) { // transients. return false; } if ("created".equals(field.getName()) == true || "lastUpdate".equals(field.getName()) == true) { return false; } return true; } }