/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
//         www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard ([email protected])
//
// 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.Field;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.persistence.Id;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.PredicateUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.log4j.Logger;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.util.Version;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.search.FullTextQuery;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.search.annotations.DocumentId;
import org.projectforge.access.AccessChecker;
import org.projectforge.access.AccessException;
import org.projectforge.access.OperationType;
import org.projectforge.common.BeanHelper;
import org.projectforge.common.DateFormats;
import org.projectforge.common.DateHelper;
import org.projectforge.common.DateHolder;
import org.projectforge.database.DatabaseDao;
import org.projectforge.lucene.ClassicAnalyzer;
import org.projectforge.user.PFUserContext;
import org.projectforge.user.PFUserDO;
import org.projectforge.user.UserGroupCache;
import org.projectforge.user.UserRight;
import org.projectforge.user.UserRightId;
import org.projectforge.user.UserRights;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import de.micromata.hibernate.history.Historizable;
import de.micromata.hibernate.history.HistoryAdapter;
import de.micromata.hibernate.history.HistoryEntry;
import de.micromata.hibernate.history.HistoryUserRetriever;
import de.micromata.hibernate.history.delta.PropertyDelta;

/**
 * 
 * @author Kai Reinhard ([email protected])
 * 
 */
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public abstract class BaseDao<O extends ExtendedBaseDO< ? extends Serializable>> extends HibernateDaoSupport implements IDao<O>
{
  public static final String EXCEPTION_HISTORIZABLE_NOTDELETABLE = "Could not delete of Historizable objects (contact your software developer): ";

  /**
   * @see Version#LUCENE_31
   */
  public static final Version LUCENE_VERSION = Version.LUCENE_31;

  /**
   * Maximum allowed mass updates within one massUpdate call.
   */
  public static final int MAX_MASS_UPDATE = 100;

  public static final String MAX_MASS_UPDATE_EXCEEDED_EXCEPTION_I18N = "massUpdate.error.maximumNumberOfAllowedMassUpdatesExceeded";

  private static final List<DisplayHistoryEntry> EMPTY_HISTORY_ENTRIES = new ArrayList<DisplayHistoryEntry>();

  private static final Logger log = Logger.getLogger(BaseDao.class);

  private static final String[] luceneReservedWords = { "AND", "OR", "NOT"};

  /**
   * Additional allowed characters (not at first position) for search string modification with wildcards. Do not forget to update
   * I18nResources.properties and the user documentation after any changes. <br/>
   * ALLOWED_CHARS =
   * @._-+*
   */
  public static final String ALLOWED_CHARS = "@._-+*";

  /**
   * Additional allowed characters (at first position) for search string modification with wildcards. Do not forget to update
   * I18nResources.properties and the user documentation after any changes. <br/>
   * ALLOWED_BEGINNING_CHARS =
   * @._
   */
  public static final String ALLOWED_BEGINNING_CHARS = "@._*";

  /**
   * If the search string contains any of this escape chars, no string modification will be done.
   */
  public static final String ESCAPE_CHARS = "+-";

  private static final String[] HISTORY_SEARCH_FIELDS = { "delta.oldValue", "delta.newValue"};

  protected Class<O> clazz;

  protected AccessChecker accessChecker;

  protected DatabaseDao databaseDao;

  protected UserGroupCache userGroupCache;

  protected HistoryAdapter historyAdapter;

  protected TransactionTemplate txTemplate;

  protected String[] searchFields;

  protected BaseDaoReindexRegistry baseDaoReindexRegistry = BaseDaoReindexRegistry.getSingleton();

  protected UserRightId userRightId = null;

  /**
   * Should the id check (on null) be avoided before save (in save method)? This is use-full if the derived dao manages the id itself (as e.
   * g. KundeDao, Kost2ArtDao).
   */
  protected boolean avoidNullIdCheckBeforeSave;

  /**
   * Set this to true if you overload {@link #afterUpdate(ExtendedBaseDO, ExtendedBaseDO)} and you need the origin data base entry in this
   * method.
   */
  protected boolean supportAfterUpdate = false;

  /**
   * Get all declared hibernate search fields. These fields are defined over annotations in the database object class. The names are the
   * property names or, if defined the name declared in the annotation of a field. <br/>
   * The user can search in these fields explicit by typing e. g. authors:beck (<field>:<searchString>)
   */
  public synchronized String[] getSearchFields()
  {
    if (searchFields != null) {
      return searchFields;
    }
    final Field[] fields = BeanHelper.getAllDeclaredFields(clazz);
    final Set<String> fieldNames = new TreeSet<String>();
    for (final Field field : fields) {
      if (field.isAnnotationPresent(org.hibernate.search.annotations.Field.class) == true) {
        // @Field(index = Index.TOKENIZED),
        final org.hibernate.search.annotations.Field annotation = field.getAnnotation(org.hibernate.search.annotations.Field.class);
        fieldNames.add(getSearchName(field.getName(), annotation));
      } else if (field.isAnnotationPresent(org.hibernate.search.annotations.Fields.class) == true) {
        // @Fields( {
        // @Field(index = Index.TOKENIZED),
        // @Field(name = "name_forsort", index = Index.UN_TOKENIZED)
        // } )
        final org.hibernate.search.annotations.Fields annFields = field.getAnnotation(org.hibernate.search.annotations.Fields.class);
        for (final org.hibernate.search.annotations.Field annotation : annFields.value()) {
          fieldNames.add(getSearchName(field.getName(), annotation));
        }
      } else if (field.isAnnotationPresent(Id.class) == true) {
        fieldNames.add(field.getName());
      } else if (field.isAnnotationPresent(DocumentId.class) == true) {
        fieldNames.add(field.getName());
      }
    }
    final Method[] methods = clazz.getMethods();
    for (final Method method : methods) {
      if (method.isAnnotationPresent(org.hibernate.search.annotations.Field.class) == true) {
        final org.hibernate.search.annotations.Field annotation = method.getAnnotation(org.hibernate.search.annotations.Field.class);
        fieldNames.add(getSearchName(method.getName(), annotation));
      } else if (method.isAnnotationPresent(DocumentId.class) == true) {
        final String prop = BeanHelper.determinePropertyName(method);
        fieldNames.add(prop);
      }
    }
    if (getAdditionalSearchFields() != null) {
      for (final String str : getAdditionalSearchFields()) {
        fieldNames.add(str);
      }
    }
    searchFields = new String[fieldNames.size()];
    fieldNames.toArray(searchFields);
    log.info("Search fields for '" + clazz + "': " + ArrayUtils.toString(searchFields));
    return searchFields;
  }

  /**
   * Overwrite this method for adding search fields manually (e. g. for embedded objects). For example see TimesheetDao.
   */
  protected String[] getAdditionalSearchFields()
  {
    return null;
  }

  private String getSearchName(final String fieldName, final org.hibernate.search.annotations.Field annotation)
  {
    if (StringUtils.isNotEmpty(annotation.name()) == true) {
      // Name of field is changed for hibernate-search via annotation:
      return annotation.name();
    } else {
      return fieldName;
    }
  }

  public void setTxTemplate(final TransactionTemplate txTemplate)
  {
    this.txTemplate = txTemplate;
  }

  public void setAccessChecker(final AccessChecker accessChecker)
  {
    this.accessChecker = accessChecker;
  }

  public void setDatabaseDao(final DatabaseDao databaseDao)
  {
    this.databaseDao = databaseDao;
  }

  public void setUserGroupCache(final UserGroupCache userGroupCache)
  {
    this.userGroupCache = userGroupCache;
  }

  public void setHistoryAdapter(final HistoryAdapter historyAdapter)
  {
    this.historyAdapter = historyAdapter;
  }

  @Override
  protected void initDao()
  {
  }

  /**
   * The setting of the DO class is required.
   * @param clazz
   */
  protected BaseDao(final Class<O> clazz)
  {
    this.clazz = clazz;
  }

  public Class<O> getDOClass()
  {
    return this.clazz;
  }

  public abstract O newInstance();

  /**
   * getOrLoad checks first weather the id is valid or not. Default implementation: id != 0 && id &gt; 0. Overload this, if the id of the DO
   * can be 0 for example.
   * @param id
   * @return
   */
  protected boolean isIdValid(final Integer id)
  {
    return (id != null && id > 0);
  }

  /**
   * If the user has select access then the object will be returned. If not, the hibernate proxy object will be get via getSession().load();
   * @param id
   */
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public O getOrLoad(final Integer id)
  {
    if (isIdValid(id) == false) {
      return null;
    } else {
      final O obj = internalGetById(id);
      if (obj == null) {
        throw new RuntimeException("Object with id " + id + " not found for class " + clazz);
      }
      if (hasLoggedInUserSelectAccess(obj, false) == true) {
        return obj;
      }
    }
    @SuppressWarnings("unchecked")
    final O result = (O) getSession().load(clazz, id);
    return result;
  }

  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public List<O> internalLoadAll()
  {
    @SuppressWarnings("unchecked")
    final List<O> list = getHibernateTemplate().find("from " + clazz.getSimpleName() + " t");
    return list;
  }

  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public List<O> internalLoad(final Collection< ? extends Serializable> idList)
  {
    if (idList == null) {
      return null;
    }
    final Session session = getSession();
    final Criteria criteria = session.createCriteria(clazz).add(Restrictions.in("id", idList));
    @SuppressWarnings("unchecked")
    final List<O> list = selectUnique(criteria.list());
    return list;
  }

  /**
   * This method is used by the searchDao and calls {@link #getList(BaseSearchFilter)} by default.
   * @param filter
   * @return A list of found entries or empty list. PLEASE NOTE: Returns null only if any error occured.
   * @see #getList(BaseSearchFilter)
   */
  public List<O> getListForSearchDao(final BaseSearchFilter filter)
  {
    return getList(filter);
  }

  /**
   * Builds query filter by simply calling constructor of QueryFilter with given search filter and calls getList(QueryFilter). Override this
   * method for building more complex query filters.
   * @param filter
   * @return A list of found entries or empty list. PLEASE NOTE: Returns null only if any error occured.
   */
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public List<O> getList(final BaseSearchFilter filter)
  {
    final QueryFilter queryFilter = new QueryFilter(filter);
    return getList(queryFilter);
  }

  /**
   * Gets the list filtered by the given filter.
   * @param filter
   */
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public List<O> getList(final QueryFilter filter) throws AccessException
  {
    checkLoggedInUserSelectAccess();
    if (accessChecker.isRestrictedUser() == true) {
      return null;
    }
    List<O> list = internalGetList(filter);
    if (list == null || list.size() == 0) {
      return list;
    }
    list = extractEntriesWithSelectAccess(list);
    return sort(list);
  }

  /**
   * Gets the list filtered by the given filter.
   * @param filter
   */
  @SuppressWarnings("unchecked")
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public List<O> internalGetList(final QueryFilter filter) throws AccessException
  {
    final BaseSearchFilter searchFilter = filter.getFilter();
    filter.clearErrorMessage();
    if (searchFilter.isIgnoreDeleted() == false) {
      filter.add(Restrictions.eq("deleted", searchFilter.isDeleted()));
    }
    if (searchFilter.getModifiedSince() != null) {
      filter.add(Restrictions.ge("lastUpdate", searchFilter.getModifiedSince()));
    }

    List<O> list = null;
    {
      final Criteria criteria = filter.buildCriteria(getSession(), clazz);
      setCacheRegion(criteria);
      if (searchFilter.isSearchNotEmpty() == true) {
        final String searchString = modifySearchString(searchFilter.getSearchString());
        final String[] searchFields = searchFilter.getSearchFields() != null ? searchFilter.getSearchFields() : getSearchFields();
        try {
          final FullTextSession fullTextSession = Search.getFullTextSession(getSession());
          final org.apache.lucene.search.Query query = createFullTextQuery(searchFields, filter, searchString);
          if (query == null) {
            // An error occured:
            return new ArrayList<O>();
          }
          final FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, clazz);
          fullTextQuery.setCriteriaQuery(criteria);
          list = fullTextQuery.list(); // return a list of managed objects
        } catch (final Exception ex) {
          final String errorMsg = "Lucene error message: "
              + ex.getMessage()
              + " (for "
              + this.getClass().getSimpleName()
              + ": "
              + searchString
              + ").";
          filter.setErrorMessage(errorMsg);
          log.info(errorMsg);
        }
      } else {
        list = criteria.list();
      }
      if (list != null) {
        list = selectUnique(list);
        if (list.size() > 0 && searchFilter.isUseModificationFilter() == true) {
          // Search now all history entries which were modified by the given user and/or in the given time period.
          final Set<Integer> idSet = getHistoryEntries(getSession(), searchFilter, false);
          final List<O> result = new ArrayList<O>();
          for (final O entry : list) {
            if (contains(idSet, entry) == true) {
              result.add(entry);
            }
          }
          list = result;
        }
      }
    }
    if (searchFilter.isSearchHistory() == true && searchFilter.isSearchNotEmpty() == true) {
      // Search now all history for the given search string.
      final Set<Integer> idSet = getHistoryEntries(getSession(), searchFilter, true);
      if (CollectionUtils.isNotEmpty(idSet) == true) {
        for (final O entry : list) {
          if (idSet.contains(entry.getId()) == true) {
            idSet.remove(entry.getId()); // Object does already exist in list.
          }
        }
        if (idSet.isEmpty() == false) {
          final Criteria criteria = filter.buildCriteria(getSession(), clazz);
          setCacheRegion(criteria);
          criteria.add(Restrictions.in("id", idSet));
          final List<O> historyMatchingEntities = criteria.list();
          list.addAll(historyMatchingEntities);
        }
      }
    }
    if (list == null) {
      // History search without search string.
      list = new ArrayList<O>();
    }
    return list;
  }

  private org.apache.lucene.search.Query createFullTextQuery(final String[] searchFields, final QueryFilter queryFilter,
      final String searchString)
  {
    final MultiFieldQueryParser parser = new MultiFieldQueryParser(LUCENE_VERSION, searchFields, new ClassicAnalyzer(Version.LUCENE_31));
    parser.setAllowLeadingWildcard(true);
    org.apache.lucene.search.Query query = null;
    try {
      query = parser.parse(searchString);
    } catch (final org.apache.lucene.queryParser.ParseException ex) {
      final String errorMsg = "Lucene error message: "
          + ex.getMessage()
          + " (for "
          + this.getClass().getSimpleName()
          + ": "
          + searchString
          + ").";
      if (queryFilter != null) {
        queryFilter.setErrorMessage(errorMsg);
      }
      log.info(errorMsg);
      return null;
    }
    return query;
  }

  /**
   * idSet.contains(entry.getId()) at default.
   * @param idSet
   * @param entry
   * @see org.projectforge.fibu.AuftragDao#contains(Set, org.projectforge.fibu.AuftragDO)
   */
  protected boolean contains(final Set<Integer> idSet, final O entry)
  {
    if (idSet == null) {
      return false;
    }
    return idSet.contains(entry.getId());
  }

  protected List<O> selectUnique(final List<O> list)
  {
    @SuppressWarnings("unchecked")
    final List<O> result = (List<O>) CollectionUtils.select(list, PredicateUtils.uniquePredicate());
    return result;
  }

  protected List<O> extractEntriesWithSelectAccess(final List<O> origList)
  {
    final List<O> result = new ArrayList<O>();
    for (final O obj : origList) {
      if (hasLoggedInUserSelectAccess(obj, false) == true) {
        result.add(obj);
        afterLoad(obj);
      }
    }
    return result;
  }

  /**
   * Overwrite this method for own list sorting. This method returns only the given list.
   * @param list
   */
  protected List<O> sort(final List<O> list)
  {
    return list;
  }

  /**
   * @see #modifySearchString(String, boolean)
   */
  public static String modifySearchString(final String searchString)
  {
    return modifySearchString(searchString, false);
  }

  /**
   * If the search string starts with "'" then the searchString will be returned without leading "'". If the search string consists only of
   * alphanumeric characters and allowed chars and spaces the wild card character '*' will be appended for enable ...* search. Otherwise the
   * searchString itself will be returned.
   * @param searchString
   * @param andSearch If true then all terms must match (AND search), otherwise OR will used (default)
   * @see #ALLOWED_CHARS
   * @see #ALLOWED_BEGINNING_CHARS
   * @see #ESCAPE_CHARS
   * @return The modified search string or the original one if no modification was done.
   */
  public static String modifySearchString(final String searchString, final boolean andSearch)
  {
    if (searchString == null) {
      return "";
    }
    if (searchString.startsWith("'") == true) {
      return searchString.substring(1);
    }
    for (int i = 0; i < searchString.length(); i++) {
      final char ch = searchString.charAt(i);
      if (Character.isLetterOrDigit(ch) == false && Character.isWhitespace(ch) == false) {
        final String allowed = (i == 0) ? ALLOWED_BEGINNING_CHARS : ALLOWED_CHARS;
        if (allowed.indexOf(ch) < 0) {
          return searchString;
        }
      }
    }
    final String[] tokens = StringUtils.split(searchString, ' ');
    final StringBuffer buf = new StringBuffer();
    boolean first = true;
    for (final String token : tokens) {
      if (first == true) {
        first = false;
      } else {
        buf.append(" ");
      }
      if (ArrayUtils.contains(luceneReservedWords, token) == false) {
        final String modified = modifySearchToken(token);
        if (tokens.length > 1 && andSearch == true && StringUtils.containsNone(modified, ESCAPE_CHARS) == true) {
          buf.append("+");
        }
        buf.append(modified);
        if (modified.endsWith("*") == false && StringUtils.containsNone(modified, ESCAPE_CHARS) == true) {
          if (andSearch == false || tokens.length > 1) {
            // Don't append '*' if used by SearchForm and only one token is given. It's will be appended automatically by BaseDao before the
            // search is executed.
            buf.append('*');
          }
        }
      } else {
        buf.append(token);
      }
    }
    return buf.toString();
  }

  /**
   * Does nothing (because it seems to be work better in most times). Quotes special Lucene characters: '-' -> "\-"
   * @param searchToken One word / token of the search string (one entry of StringUtils.split(searchString, ' ')).
   * @return
   */
  protected static String modifySearchToken(final String searchToken)
  {
    final StringBuffer buf = new StringBuffer();
    for (int i = 0; i < searchToken.length(); i++) {
      final char ch = searchToken.charAt(i);
      /*
       * if (ESCAPE_CHARS.indexOf(ch) >= 0) { buf.append('\\'); }
       */
      buf.append(ch);
    }
    return buf.toString();
  }

  /**
   * @param id primary key of the base object.
   */
  @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
  public O getById(final Serializable id) throws AccessException
  {
    if (accessChecker.isRestrictedUser() == true) {
      return null;
    }
    checkLoggedInUserSelectAccess();
    final O obj = internalGetById(id);
    if (obj == null) {
      return null;
    }
    checkLoggedInUserSelectAccess(obj);
    return obj;
  }

  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public O internalGetById(final Serializable id)
  {
    if (id == null) {
      return null;
    }
    final O obj = getHibernateTemplate().get(clazz, id, LockMode.READ);
    afterLoad(obj);
    return obj;
  }

  /**
   * Gets the history entries of the object.
   * @param obj
   */
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public HistoryEntry[] getHistoryEntries(final O obj)
  {
    accessChecker.checkRestrictedUser();
    checkLoggedInUserHistoryAccess(obj);
    return internalGetHistoryEntries(obj);
  }

  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public HistoryEntry[] internalGetHistoryEntries(final BaseDO< ? > obj)
  {
    accessChecker.checkRestrictedUser();
    final HistoryAdapter ad = new HistoryAdapter();
    ad.setSessionFactory(getHibernateTemplate().getSessionFactory());
    return ad.getHistoryEntries(obj);
  }

  /**
   * Gets the history entries of the object in flat format.<br/>
   * Please note: If user has no access an empty list will be returned.
   * @param obj
   */
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public List<DisplayHistoryEntry> getDisplayHistoryEntries(final O obj)
  {
    if (obj.getId() == null || hasLoggedInUserHistoryAccess(obj, false) == false) {
      return EMPTY_HISTORY_ENTRIES;
    }
    final List<DisplayHistoryEntry> result = getHibernateTemplate().execute(new HibernateCallback<List<DisplayHistoryEntry>>() {
      public List<DisplayHistoryEntry> doInHibernate(final Session session) throws HibernateException, SQLException
      {
        final HistoryEntry[] entries = getHistoryEntries(obj);
        if (entries == null) {
          return null;
        }
        return convertAll(entries, session);
      }
    });
    return result;
  }

  public List<DisplayHistoryEntry> internalGetDisplayHistoryEntries(final BaseDO< ? > obj)
  {
    accessChecker.checkRestrictedUser();
    final List<DisplayHistoryEntry> result = getHibernateTemplate().execute(new HibernateCallback<List<DisplayHistoryEntry>>() {
      public List<DisplayHistoryEntry> doInHibernate(final Session session) throws HibernateException, SQLException
      {
        final HistoryEntry[] entries = internalGetHistoryEntries(obj);
        if (entries == null) {
          return null;
        }
        return convertAll(entries, session);
      }
    });
    return result;
  }

  protected List<DisplayHistoryEntry> convertAll(final HistoryEntry[] entries, final Session session)
  {
    final List<DisplayHistoryEntry> list = new ArrayList<DisplayHistoryEntry>();
    for (final HistoryEntry entry : entries) {
      final List<DisplayHistoryEntry> l = convert(entry, session);
      list.addAll(l);
    }
    return list;
  }

  public List<DisplayHistoryEntry> convert(final HistoryEntry entry, final Session session)
  {
    final List<DisplayHistoryEntry> result = new ArrayList<DisplayHistoryEntry>();
    final List<PropertyDelta> delta = entry.getDelta();
    if (delta == null || delta.size() == 0) {
      final DisplayHistoryEntry se = new DisplayHistoryEntry(userGroupCache, entry);
      result.add(se);
    } else {
      for (final PropertyDelta prop : delta) {
        final DisplayHistoryEntry se = new DisplayHistoryEntry(userGroupCache, entry, prop, session);
        result.add(se);
      }
    }
    return result;
  }

  /**
   * Gets the history entries of the object in flat format.<br/>
   * Please note: No check access will be done! Please check the access before getting the object.
   * @param obj
   */
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public List<SimpleHistoryEntry> getSimpleHistoryEntries(final O obj)
  {
    final List<SimpleHistoryEntry> result = getHibernateTemplate().execute(new HibernateCallback<List<SimpleHistoryEntry>>() {
      public List<SimpleHistoryEntry> doInHibernate(final Session session) throws HibernateException, SQLException
      {
        final HistoryEntry[] entries = getHistoryEntries(obj);
        if (entries == null) {
          return null;
        }
        final List<SimpleHistoryEntry> list = new ArrayList<SimpleHistoryEntry>();
        for (final HistoryEntry entry : entries) {
          final List<PropertyDelta> delta = entry.getDelta();
          if (delta == null || delta.size() == 0) {
            final SimpleHistoryEntry se = new SimpleHistoryEntry(userGroupCache, entry);
            list.add(se);
          } else {
            for (final PropertyDelta prop : delta) {
              final SimpleHistoryEntry se = new SimpleHistoryEntry(userGroupCache, entry, prop);
              list.add(se);
            }
          }
        }
        return list;
      }
    });
    return result;
  }

  /**
   * @param obj
   * @return the generated identifier, if save method is used, otherwise null.
   * @throws AccessException
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
  public Serializable saveOrUpdate(final O obj) throws AccessException
  {
    Serializable id = null;
    if (obj.getId() != null) {
      update(obj);
    } else {
      id = save(obj);
    }
    return id;
  }

  /**
   * @param obj
   * @return the generated identifier, if save method is used, otherwise null.
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
  public Serializable internalSaveOrUpdate(final O obj)
  {
    Serializable id = null;
    if (obj.getId() != null) {
      internalUpdate(obj);
    } else {
      id = internalSave(obj);
    }
    return id;
  }

  /**
   * Call save(O) for every object in the given list.
   * @param objects
   * @throws AccessException
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
  public void save(final List<O> objects) throws AccessException
  {
    Validate.notNull(objects);
    for (final O obj : objects) {
      save(obj);
    }
  }

  /**
   * 
   * @param obj
   * @return the generated identifier.
   * @throws AccessException
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
  public Serializable save(final O obj) throws AccessException
  {
    Validate.notNull(obj);
    if (avoidNullIdCheckBeforeSave == false) {
      Validate.isTrue(obj.getId() == null);
    }
    checkLoggedInUserInsertAccess(obj);
    accessChecker.checkRestrictedOrDemoUser();
    return internalSave(obj);
  }

  /**
   * This method will be called after loading an object from the data base. Does nothing at default. This method is not called by
   * internalLoadAll.
   */
  protected void afterLoad(final O obj)
  {

  }

  /**
   * This method will be called after inserting, updating, deleting or marking the data object as deleted. This method is for example needed
   * for expiring the UserGroupCache after inserting or updating a user or group data object. Does nothing at default.
   */
  protected void afterSaveOrModify(final O obj)
  {
  }

  /**
   * This method will be called after inserting. Does nothing at default.
   * @param obj The inserted object
   */
  protected void afterSave(final O obj)
  {
  }

  /**
   * This method will be called before inserting. Does nothing at default.
   */
  protected void onSave(final O obj)
  {
  }

  /**
   * This method will be called before inserting, updating, deleting or marking the data object as deleted. Does nothing at default.
   */
  protected void onSaveOrModify(final O obj)
  {
  }

  /**
   * This method will be called after updating. Does nothing at default. PLEASE NOTE: If you overload this method don't forget to set
   * {@link #supportAfterUpdate} to true, otherwise you won't get the origin data base object!
   * @param obj The modified object
   * @param dbObj The object from data base before modification.
   */
  protected void afterUpdate(final O obj, final O dbObj)
  {
  }

  /**
   * This method will be called after updating. Does nothing at default. PLEASE NOTE: If you overload this method don't forget to set
   * {@link #supportAfterUpdate} to true, otherwise you won't get the origin data base object!
   * @param obj The modified object
   * @param dbObj The object from data base before modification.
   * @param isModified is true if the object was changed, false if the object wasn't modified.
   */
  protected void afterUpdate(final O obj, final O dbObj, final boolean isModified)
  {
  }

  /**
   * This method will be called before updating the data object. Will also called if in internalUpdate no modification was detected. Please
   * note: Do not modify the object oldVersion! Does nothing at default.
   * @param obj The changed object.
   * @param dbObj The current data base version of this object.
   */
  protected void onChange(final O obj, final O dbObj)
  {
  }

  /**
   * This method will be called before deleting. Does nothing at default.
   * @param obj The deleted object.
   */
  protected void onDelete(final O obj)
  {
  }

  /**
   * This method will be called after deleting. Does nothing at default.
   * @param obj The deleted object.
   */
  protected void afterDelete(final O obj)
  {
  }

  /**
   * This method will be called after undeleting. Does nothing at default.
   * @param obj The deleted object.
   */
  protected void afterUndelete(final O obj)
  {
  }

  /**
   * This method is for internal use e. g. for updating objects without check access.
   * @param obj
   * @return the generated identifier.
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
  public Serializable internalSave(final O obj)
  {
    Validate.notNull(obj);
    obj.setCreated();
    obj.setLastUpdate();
    onSave(obj);
    onSaveOrModify(obj);
    final Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
    final Serializable id = session.save(obj);
    log.info("New object added (" + id + "): " + obj.toString());
    prepareHibernateSearch(obj, OperationType.INSERT);
    session.flush();
    Search.getFullTextSession(session).flushToIndexes();
    afterSaveOrModify(obj);
    afterSave(obj);
    return id;
  }

  @Transactional(readOnly = false, propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
  public void saveOrUpdate(final Collection<O> col)
  {
    for (final O obj : col) {
      saveOrUpdate(obj);
    }
  }

  @Transactional(readOnly = true, propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
  public void saveOrUpdate(final BaseDao<O> currentProxy, final Collection<O> col, final int blockSize)
  {
    final List<O> list = new ArrayList<O>();
    int counter = 0;
    // final BaseDao<O> currentProxy = (BaseDao<O>) AopContext.currentProxy();
    for (final O obj : col) {
      list.add(obj);
      if (++counter >= blockSize) {
        counter = 0;
        currentProxy.saveOrUpdate(list);
        list.clear();
      }
    }
    currentProxy.saveOrUpdate(list);
  }

  @Transactional(readOnly = false, propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
  public void internalSaveOrUpdate(final Collection<O> col)
  {
    for (final O obj : col) {
      internalSaveOrUpdate(obj);
    }
  }

  @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
  public void internalSaveOrUpdate(final BaseDao<O> currentProxy, final Collection<O> col, final int blockSize)
  {
    final List<O> list = new ArrayList<O>();
    int counter = 0;
    // final BaseDao<O> currentProxy = (BaseDao<O>) AopContext.currentProxy();
    for (final O obj : col) {
      list.add(obj);
      if (++counter >= blockSize) {
        counter = 0;
        currentProxy.internalSaveOrUpdate(list);
        list.clear();
      }
    }
    currentProxy.internalSaveOrUpdate(list);
  }

  /**
   * @param obj
   * @throws AccessException
   * @return true, if modifications were done, false if no modification detected.
   * @see #internalUpdate(ExtendedBaseDO, boolean)
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
  public ModificationStatus update(final O obj) throws AccessException
  {
    Validate.notNull(obj);
    if (obj.getId() == null) {
      final String msg = "Could not update object unless id is not given:" + obj.toString();
      log.error(msg);
      throw new RuntimeException(msg);
    }
    return internalUpdate(obj, true);
  }

  /**
   * This method is for internal use e. g. for updating objects without check access.
   * @param obj
   * @return true, if modifications were done, false if no modification detected.
   * @see #internalUpdate(ExtendedBaseDO, boolean)
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
  public ModificationStatus internalUpdate(final O obj)
  {
    return internalUpdate(obj, false);
  }

  /**
   * This method is for internal use e. g. for updating objects without check access.<br/>
   * Please note: update ignores the field deleted. Use markAsDeleted, delete and undelete methods instead.
   * @param obj
   * @param checkAccess If false, any access check will be ignored.
   * @return true, if modifications were done, false if no modification detected.
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
  public ModificationStatus internalUpdate(final O obj, final boolean checkAccess)
  {
    onSaveOrModify(obj);
    if (checkAccess == true) {
      accessChecker.checkRestrictedOrDemoUser();
    }
    final O dbObj = getHibernateTemplate().load(clazz, obj.getId(), LockMode.PESSIMISTIC_WRITE);
    if (checkAccess == true) {
      checkLoggedInUserUpdateAccess(obj, dbObj);
    }
    onChange(obj, dbObj);
    final O dbObjBackup;
    if (supportAfterUpdate == true) {
      dbObjBackup = getBackupObject(dbObj);
    } else {
      dbObjBackup = null;
    }
    final boolean wantsReindexAllDependentObjects = wantsReindexAllDependentObjects(obj, dbObj);
    // Copy all values of modified user to database object, ignore field 'deleted'.
    final ModificationStatus result = copyValues(obj, dbObj, "deleted");
    if (result != ModificationStatus.NONE) {
      dbObj.setLastUpdate();
      log.info("Object updated: " + dbObj.toString());
    } else {
      log.info("No modifications detected (no update needed): " + dbObj.toString());
    }
    prepareHibernateSearch(obj, OperationType.UPDATE);
    final Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
    session.flush();
    Search.getFullTextSession(session).flushToIndexes();
    afterSaveOrModify(obj);
    if (supportAfterUpdate == true) {
      afterUpdate(obj, dbObjBackup, result != ModificationStatus.NONE);
      afterUpdate(obj, dbObjBackup);
    } else {
      afterUpdate(obj, null, result != ModificationStatus.NONE);
      afterUpdate(obj, null);
    }
    if (wantsReindexAllDependentObjects == true) {
      reindexDependentObjects(obj);
    }
    return result;
  }

  /**
   * @return If true (default if not minor Change) all dependent data-base objects will be re-indexed. For e. g. PFUserDO all time-sheets
   *         etc. of this user will be re-indexed. It's called after internalUpdate. Refer UserDao to see more.
   * @see BaseDO#isMinorChange()
   */
  protected boolean wantsReindexAllDependentObjects(final O obj, final O dbObj)
  {
    return obj.isMinorChange() == false;
  }

  /**
   * Used by internal update if supportAfterUpdate is true for storing db object version for afterUpdate. Override this method to implement
   * your own copy method.
   * @param dbObj
   * @return
   */
  protected O getBackupObject(final O dbObj)
  {
    final O backupObj = newInstance();
    copyValues(dbObj, backupObj);
    return backupObj;
  }

  /**
   * Overwrite this method if you have lazy exceptions while Hibernate-Search re-indexes. See e. g. AuftragDao.
   * @param obj
   */
  protected void prepareHibernateSearch(final O obj, final OperationType operationType)
  {
  }

  /**
   * Object will be marked as deleted (boolean flag), therefore undelete is always possible without any loss of data.
   * @param obj
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
  public void markAsDeleted(final O obj) throws AccessException
  {
    Validate.notNull(obj);
    if (obj.getId() == null) {
      final String msg = "Could not delete object unless id is not given:" + obj.toString();
      log.error(msg);
      throw new RuntimeException(msg);
    }
    final O dbObj = getHibernateTemplate().load(clazz, obj.getId(), LockMode.PESSIMISTIC_WRITE);
    checkLoggedInUserDeleteAccess(obj, dbObj);
    accessChecker.checkRestrictedOrDemoUser();
    internalMarkAsDeleted(obj);
  }

  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
  public void internalMarkAsDeleted(final O obj)
  {
    if (obj instanceof Historizable == false) {
      log.error("Object is not historizable. Therefore marking as deleted is not supported. Please use delete instead.");
      throw new InternalErrorException();
    }
    onDelete(obj);
    final O dbObj = getHibernateTemplate().load(clazz, obj.getId(), LockMode.PESSIMISTIC_WRITE);
    onSaveOrModify(obj);
    copyValues(obj, dbObj, "deleted"); // If user has made additional changes.
    dbObj.setDeleted(true);
    dbObj.setLastUpdate();
    final Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
    session.flush();
    Search.getFullTextSession(session).flushToIndexes();
    afterSaveOrModify(obj);
    afterDelete(obj);
    getSession().flush();
    log.info("Object marked as deleted: " + dbObj.toString());
  }

  /**
   * Object will be deleted finally out of the data base.
   * @param obj
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
  public void delete(final O obj) throws AccessException
  {
    Validate.notNull(obj);
    if (obj instanceof Historizable) {
      final String msg = EXCEPTION_HISTORIZABLE_NOTDELETABLE + obj.toString();
      log.error(msg);
      throw new RuntimeException(msg);
    }
    if (obj.getId() == null) {
      final String msg = "Could not destroy object unless id is not given: " + obj.toString();
      log.error(msg);
      throw new RuntimeException(msg);
    }
    accessChecker.checkRestrictedOrDemoUser();
    onDelete(obj);
    final O dbObj = getHibernateTemplate().load(clazz, obj.getId(), LockMode.PESSIMISTIC_WRITE);
    checkLoggedInUserDeleteAccess(obj, dbObj);
    getHibernateTemplate().delete(dbObj);
    log.info("Object deleted: " + obj.toString());
    afterSaveOrModify(obj);
    afterDelete(obj);
  }

  /**
   * Object will be marked as deleted (booelan flag), therefore undelete is always possible without any loss of data.
   * @param obj
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
  public void undelete(final O obj) throws AccessException
  {
    Validate.notNull(obj);
    if (obj.getId() == null) {
      final String msg = "Could not undelete object unless id is not given:" + obj.toString();
      log.error(msg);
      throw new RuntimeException(msg);
    }
    checkLoggedInUserInsertAccess(obj);
    accessChecker.checkRestrictedOrDemoUser();
    internalUndelete(obj);
  }

  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
  public void internalUndelete(final O obj)
  {
    final O dbObj = getHibernateTemplate().load(clazz, obj.getId(), LockMode.PESSIMISTIC_WRITE);
    onSaveOrModify(obj);
    copyValues(obj, dbObj, "deleted"); // If user has made additional changes.
    dbObj.setDeleted(false);
    obj.setDeleted(false);
    dbObj.setLastUpdate();
    obj.setLastUpdate(dbObj.getLastUpdate());
    log.info("Object undeleted: " + dbObj.toString());
    final Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
    session.flush();
    Search.getFullTextSession(session).flushToIndexes();
    afterSaveOrModify(obj);
    afterUndelete(obj);
  }

  /**
   * Checks the basic select access right. Overload this method if your class supports this right.
   */
  protected final void checkLoggedInUserSelectAccess() throws AccessException
  {
    if (hasSelectAccess(PFUserContext.getUser(), true) == false) {
      // Should not occur!
      log.error("Development error: Subclass should throw an exception instead of returning false.");
      throw new UserException(UserException.I18N_KEY_PLEASE_CONTACT_DEVELOPER_TEAM);
    }
  }

  protected final void checkLoggedInUserSelectAccess(final O obj) throws AccessException
  {
    if (hasSelectAccess(PFUserContext.getUser(), obj, true) == false) {
      // Should not occur!
      log.error("Development error: Subclass should throw an exception instead of returning false.");
      throw new UserException(UserException.I18N_KEY_PLEASE_CONTACT_DEVELOPER_TEAM);
    }
  }

  protected final void checkLoggedInUserHistoryAccess(final O obj) throws AccessException
  {
    if (hasHistoryAccess(PFUserContext.getUser(), true) == false || hasLoggedInUserHistoryAccess(obj, true) == false) {
      // Should not occur!
      log.error("Development error: Subclass should throw an exception instead of returning false.");
      throw new UserException(UserException.I18N_KEY_PLEASE_CONTACT_DEVELOPER_TEAM);
    }
  }

  protected final void checkLoggedInUserInsertAccess(final O obj) throws AccessException
  {
    checkInsertAccess(PFUserContext.getUser(), obj);
  }

  protected void checkInsertAccess(final PFUserDO user, final O obj) throws AccessException
  {
    if (hasInsertAccess(user, obj, true) == false) {
      // Should not occur!
      log.error("Development error: Subclass should throw an exception instead of returning false.");
      throw new UserException(UserException.I18N_KEY_PLEASE_CONTACT_DEVELOPER_TEAM);
    }
  }

  /**
   * @param dbObj The original object (stored in the database)
   * @param obj
   * @throws AccessException
   */
  protected final void checkLoggedInUserUpdateAccess(final O obj, final O dbObj) throws AccessException
  {
    checkUpdateAccess(PFUserContext.getUser(), obj, dbObj);
  }

  /**
   * @param dbObj The original object (stored in the database)
   * @param obj
   * @throws AccessException
   */
  protected void checkUpdateAccess(final PFUserDO user, final O obj, final O dbObj) throws AccessException
  {
    if (hasUpdateAccess(user, obj, dbObj, true) == false) {
      // Should not occur!
      log.error("Development error: Subclass should throw an exception instead of returning false.");
      throw new UserException(UserException.I18N_KEY_PLEASE_CONTACT_DEVELOPER_TEAM);
    }
  }

  protected final void checkLoggedInUserDeleteAccess(final O obj, final O dbObj) throws AccessException
  {
    if (hasLoggedInUserDeleteAccess(obj, dbObj, true) == false) {
      // Should not occur!
      log.error("Development error: Subclass should throw an exception instead of returning false.");
      throw new UserException(UserException.I18N_KEY_PLEASE_CONTACT_DEVELOPER_TEAM);
    }
  }

  /**
   * Checks the basic select access right. Overwrite this method if the basic select access should be checked.
   * @return true at default or if readWriteUserRightId is given hasReadAccess(boolean).
   * @see #hasSelectAccess(PFUserDO, boolean)
   */
  public final boolean hasLoggedInUserSelectAccess(final boolean throwException)
  {
    return hasSelectAccess(PFUserContext.getUser(), throwException);
  }

  /**
   * Checks the basic select access right. Overwrite this method if the basic select access should be checked.
   * @return true at default or if readWriteUserRightId is given hasReadAccess(boolean).
   * @see #hasAccess(PFUserDO, ExtendedBaseDO, ExtendedBaseDO, OperationType, boolean)
   */
  public boolean hasSelectAccess(final PFUserDO user, final boolean throwException)
  {
    return hasAccess(user, null, null, OperationType.SELECT, throwException);
  }

  /**
   * If userRightId is given then {@link AccessChecker#hasAccess(PFUserDO, UserRightId, Object, Object, OperationType, boolean)} is called and
   * returned. If not given a UnsupportedOperationException is thrown. Checks the user's access to the given object.
   * @param obj The object.
   * @param oldObj The old version of the object (is only given for operationType {@link OperationType#UPDATE}).
   * @param operationType The operation type (select, insert, update or delete)
   * @return true, if the user has the access right for the given operation type and object.
   */
  public final boolean hasLoggedInUserAccess(final O obj, final O oldObj, final OperationType operationType, final boolean throwException)
  {
    return hasAccess(PFUserContext.getUser(), obj, oldObj, operationType, throwException);
  }

  /**
   * If userRightId is given then {@link AccessChecker#hasAccess(PFUserDO, UserRightId, Object, Object, OperationType, boolean)} is called and
   * returned. If not given a UnsupportedOperationException is thrown. Checks the user's access to the given object.
   * @param user Check the access for the given user instead of the logged-in user.
   * @param obj The object.
   * @param oldObj The old version of the object (is only given for operationType {@link OperationType#UPDATE}).
   * @param operationType The operation type (select, insert, update or delete)
   * @return true, if the user has the access right for the given operation type and object.
   */
  public boolean hasAccess(final PFUserDO user, final O obj, final O oldObj, final OperationType operationType, final boolean throwException)
  {
    if (userRightId != null) {
      return accessChecker.hasAccess(user, userRightId, obj, oldObj, operationType, throwException);
    }
    throw new UnsupportedOperationException(
        "readWriteUserRightId not given. Override this method or set readWriteUserRightId in constructor.");
  }

  /**
   * @param obj Check access to this object.
   * @see #hasSelectAccess(PFUserDO, ExtendedBaseDO, boolean)
   */
  public final boolean hasLoggedInUserSelectAccess(final O obj, final boolean throwException)
  {
    return hasSelectAccess(PFUserContext.getUser(), obj, throwException);
  }

  /**
   * @param user Check the access for the given user instead of the logged-in user. Checks select access right by calling hasAccess(obj,
   *          OperationType.SELECT).
   * @param obj Check access to this object.
   * @see #hasAccess(PFUserDO, ExtendedBaseDO, ExtendedBaseDO, OperationType, boolean)
   */
  public boolean hasSelectAccess(final PFUserDO user, final O obj, final boolean throwException)
  {
    return hasAccess(user, obj, null, OperationType.SELECT, throwException);
  }

  /**
   * Has the user access to the history of the given object. At default this method calls hasHistoryAccess(boolean) first and then
   * hasSelectAccess.
   * @param throwException
   */
  public final boolean hasLoggedInUserHistoryAccess(final O obj, final boolean throwException)
  {
    return hasHistoryAccess(PFUserContext.getUser(), obj, throwException);
  }

  /**
   * Has the user access to the history of the given object. At default this method calls hasHistoryAccess(boolean) first and then
   * hasSelectAccess.
   * @param throwException
   */
  public boolean hasHistoryAccess(final PFUserDO user, final O obj, final boolean throwException)
  {
    if (hasHistoryAccess(user, throwException) == false) {
      return false;
    }
    if (userRightId != null) {
      return accessChecker.hasHistoryAccess(user, userRightId, obj, throwException);
    }
    return hasSelectAccess(user, obj, throwException);
  }

  /**
   * Has the user access to the history in general of the objects. At default this method calls hasSelectAccess.
   * @param throwException
   */
  public final boolean hasLoggedInUserHistoryAccess(final boolean throwException)
  {
    return hasHistoryAccess(PFUserContext.getUser(), throwException);
  }

  /**
   * Has the user access to the history in general of the objects. At default this method calls hasSelectAccess.
   * @param throwException
   */
  public boolean hasHistoryAccess(final PFUserDO user, final boolean throwException)
  {
    if (userRightId != null) {
      return accessChecker.hasHistoryAccess(user, userRightId, null, throwException);
    }
    return hasSelectAccess(user, throwException);
  }

  /**
   * Checks insert access right by calling hasAccess(obj, OperationType.INSERT).
   * @param obj Check access to this object.
   * @see #hasInsertAccess(PFUserDO, ExtendedBaseDO, boolean)
   */
  public final boolean hasLoggedInUserInsertAccess(final O obj, final boolean throwException)
  {
    return hasInsertAccess(PFUserContext.getUser(), obj, throwException);
  }

  /**
   * Checks insert access right by calling hasAccess(obj, OperationType.INSERT).
   * @param obj Check access to this object.
   * @see #hasAccess(PFUserDO, ExtendedBaseDO, ExtendedBaseDO, OperationType, boolean)
   */
  public boolean hasInsertAccess(final PFUserDO user, final O obj, final boolean throwException)
  {
    return hasAccess(user, obj, null, OperationType.INSERT, throwException);
  }

  /**
   * Checks write access of the readWriteUserRight. If not given, true is returned at default. This method should only be used for checking
   * the insert access to show an insert button or not. Before inserting any object the write access is checked by has*Access(...)
   * independent of the result of this method.
   * @see org.projectforge.core.IDao#hasInsertAccess(PFUserDO)
   */
  public final boolean hasLoggedInUserInsertAccess()
  {
    return hasInsertAccess(PFUserContext.getUser());
  }

  /**
   * Checks write access of the readWriteUserRight. If not given, true is returned at default. This method should only be used for checking
   * the insert access to show an insert button or not. Before inserting any object the write access is checked by has*Access(...)
   * independent of the result of this method.
   * @see org.projectforge.core.IDao#hasInsertAccess(PFUserDO)
   */
  public boolean hasInsertAccess(final PFUserDO user)
  {
    if (userRightId != null) {
      return accessChecker.hasInsertAccess(user, userRightId, false);
    }
    return true;
  }

  /**
   * Checks update access right by calling hasAccess(obj, OperationType.UPDATE).
   * @param dbObj The original object (stored in the database)
   * @param obj Check access to this object.
   * @see #hasUpdateAccess(PFUserDO, ExtendedBaseDO, ExtendedBaseDO, boolean)
   */
  public final boolean hasLoggedInUserUpdateAccess(final O obj, final O dbObj, final boolean throwException)
  {
    return hasUpdateAccess(PFUserContext.getUser(), obj, dbObj, throwException);
  }

  /**
   * Checks update access right by calling hasAccess(obj, OperationType.UPDATE).
   * @param dbObj The original object (stored in the database)
   * @param obj Check access to this object.
   * @see #hasAccess(PFUserDO, ExtendedBaseDO, ExtendedBaseDO, OperationType, boolean)
   */
  public boolean hasUpdateAccess(final PFUserDO user, final O obj, final O dbObj, final boolean throwException)
  {
    return hasAccess(user, obj, dbObj, OperationType.UPDATE, throwException);
  }

  /**
   * Checks delete access right by calling hasAccess(obj, OperationType.DELETE).
   * @param obj Check access to this object.
   * @param dbObj current version of this object in the data base.
   * @see #hasDeleteAccess(PFUserDO, ExtendedBaseDO, ExtendedBaseDO, boolean)
   */
  public final boolean hasLoggedInUserDeleteAccess(final O obj, final O dbObj, final boolean throwException)
  {
    return hasDeleteAccess(PFUserContext.getUser(), obj, dbObj, throwException);
  }

  /**
   * Checks delete access right by calling hasAccess(obj, OperationType.DELETE).
   * @param obj Check access to this object.
   * @param dbObj current version of this object in the data base.
   * @see #hasAccess(PFUserDO, ExtendedBaseDO, ExtendedBaseDO, OperationType, boolean)
   */
  public boolean hasDeleteAccess(final PFUserDO user, final O obj, final O dbObj, final boolean throwException)
  {
    return hasAccess(user, obj, dbObj, OperationType.DELETE, throwException);
  }

  public UserRight getUserRight()
  {
    if (userRightId != null) {
      return UserRights.instance().getRight(userRightId);
    } else {
      return null;
    }
  }

  /**
   * Overload this method for copying field manually. Used for modifiing fields inside methods: update, markAsDeleted and undelete.
   * 
   * @param src
   * @param dest
   * @return true, if any field was modified, otherwise false.
   * @see BaseDO#copyValuesFrom(BaseDO, String...)
   */
  protected ModificationStatus copyValues(final O src, final O dest, final String... ignoreFields)
  {
    return dest.copyValuesFrom(src, ignoreFields);
  }

  protected void createHistoryEntry(final Object entity, final Number id, final String property, final Class< ? > valueClass,
      final Object oldValue, final Object newValue)
  {
    accessChecker.checkRestrictedOrDemoUser();
    final PFUserDO contextUser = PFUserContext.getUser();
    final String userPk = contextUser != null ? contextUser.getId().toString() : null;
    if (userPk == null) {
      log.warn("No user found for creating history entry.");
    }
    historyAdapter.createHistoryEntry(entity, id, new HistoryUserRetriever() {
      public String getPrincipal()
      {
        return userPk;
      }
    }, property, valueClass, oldValue, newValue);
  }

  /**
   * Only generic check access will be done. The matching entries will not be checked!
   * @param property Property of the data base entity.
   * @param searchString String the user has typed in.
   * @return All matching entries (like search) for the given property modified or updated in the last 2 years.
   */
  @SuppressWarnings("unchecked")
  public List<String> getAutocompletion(final String property, final String searchString)
  {
    checkLoggedInUserSelectAccess();
    if (StringUtils.isBlank(searchString) == true) {
      return null;
    }
    final String hql = "select distinct "
        + property
        + " from "
        + clazz.getSimpleName()
        + " t where deleted=false and lastUpdate > ? and lower(t."
        + property
        + ") like ?) order by t."
        + property;
    final Query query = getSession().createQuery(hql);
    final DateHolder dh = new DateHolder();
    dh.add(Calendar.YEAR, -2); // Search only for entries of the last 2 years.
    query.setDate(0, dh.getDate());
    query.setString(1, "%" + StringUtils.lowerCase(searchString) + "%");
    final List<String> list = query.list();
    return list;
  }

  /**
   * Re-indexes the entries of the last day, 1,000 at max.
   * @see DatabaseDao#createReindexSettings(boolean)
   */
  public void rebuildDatabaseIndex4NewestEntries()
  {
    final ReindexSettings settings = DatabaseDao.createReindexSettings(true);
    databaseDao.rebuildDatabaseSearchIndices(clazz, settings);
    databaseDao.rebuildDatabaseSearchIndices(HistoryEntry.class, settings);
  }

  /**
   * Re-indexes all entries (full re-index).
   */
  public void rebuildDatabaseIndex()
  {
    final ReindexSettings settings = DatabaseDao.createReindexSettings(false);
    databaseDao.rebuildDatabaseSearchIndices(clazz, settings);
  }

  /**
   * Re-index all dependent objects manually (hibernate search). Hibernate doesn't re-index these objects, does it?
   * @param obj
   */
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
  public void reindexDependentObjects(final O obj)
  {
    HibernateSearchDependentObjectsReindexer.getSingleton().reindexDependents(getHibernateTemplate(), obj);
  }

  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
  public void massUpdate(final List<O> list, final O master)
  {
    if (list == null || list.size() == 0) {
      // No entries to update.
      return;
    }
    if (list.size() > MAX_MASS_UPDATE) {
      throw new UserException(MAX_MASS_UPDATE_EXCEEDED_EXCEPTION_I18N, new Object[] { MAX_MASS_UPDATE});
    }
    final Object store = prepareMassUpdateStore(list, master);
    for (final O entry : list) {
      if (massUpdateEntry(entry, master, store) == true) {
        try {
          update(entry);
        } catch (final Exception ex) {
          log.info("Exception occured while updating entry inside mass update: " + entry);
        }
      }
    }
  }

  /**
   * Object pass thru every massUpdateEntry call.
   * @param list
   * @param master
   * @return null if not overloaded.
   */
  protected Object prepareMassUpdateStore(final List<O> list, final O master)
  {
    return null;
  }

  /**
   * Overload this method for mass update support.
   * @param entry
   * @param master
   * @param store Object created with prepareMassUpdateStore if needed. Null at default.
   * @return true, if entry is ready for update otherwise false (no update will be done for this entry).
   */
  protected boolean massUpdateEntry(final O entry, final O master, final Object store)
  {
    throw new UnsupportedOperationException("Mass update is not supported by this dao for: " + clazz.getName());
  }

  private Set<Integer> getHistoryEntries(final Session session, final BaseSearchFilter filter, final boolean searchStringInHistory)
  {
    if (hasLoggedInUserSelectAccess(false) == false || hasLoggedInUserHistoryAccess(false) == false) {
      // User has in general no access to history entries of the given object type (clazz).
      return null;
    }
    final Set<Integer> idSet = new HashSet<Integer>();
    getHistoryEntries(session, filter, idSet, clazz, searchStringInHistory);
    if (searchStringInHistory == false) {
      if (getAdditionalHistorySearchDOs() != null) {
        for (final Class< ? > aclazz : getAdditionalHistorySearchDOs()) {
          getHistoryEntries(session, filter, idSet, aclazz, searchStringInHistory);
        }
      }
    }
    return idSet;
  }

  @SuppressWarnings("unchecked")
  private void getHistoryEntries(final Session session, final BaseSearchFilter filter, final Set<Integer> idSet, final Class< ? > clazz,
      final boolean searchStringInHistory)
  {
    if (log.isDebugEnabled() == true) {
      log.debug("Searching in " + clazz);
    }
    // First get all history entries matching the filter and the given class.
    final String className = ClassUtils.getShortClassName(clazz);
    if (searchStringInHistory == true) {
      final StringBuffer buf = new StringBuffer();
      buf.append("(+className:").append(className);
      if (filter.getStartTimeOfModification() != null || filter.getStopTimeOfModification() != null) {
        final DateFormat df = new SimpleDateFormat(DateFormats.LUCENE_TIMESTAMP_MINUTE);
        df.setTimeZone(DateHelper.UTC);
        buf.append(" +timestamp:[");
        if (filter.getStartTimeOfModification() != null) {
          buf.append(df.format(filter.getStartTimeOfModification()));
        } else {
          buf.append("000000000000");
        }
        buf.append(" TO ");
        if (filter.getStopTimeOfModification() != null) {
          buf.append(df.format(filter.getStopTimeOfModification()));
        } else {
          buf.append("999999999999");
        }
        buf.append("]");
      }
      if (filter.getModifiedByUserId() != null) {
        buf.append(" +userName:").append(filter.getModifiedByUserId());
      }
      buf.append(") AND (");
      final String searchString = buf.toString() + modifySearchString(filter.getSearchString()) + ")";
      try {
        final FullTextSession fullTextSession = Search.getFullTextSession(getSession());
        final org.apache.lucene.search.Query query = createFullTextQuery(HISTORY_SEARCH_FIELDS, null, searchString);
        if (query == null) {
          // An error occured:
          return;
        }
        final FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, HistoryEntry.class);
        fullTextQuery.setCacheable(true);
        fullTextQuery.setCacheRegion("historyItemCache");
        fullTextQuery.setProjection("entityId");
        final List<Object[]> result = fullTextQuery.list();
        if (result != null && result.size() > 0) {
          for (final Object[] oa : result) {
            idSet.add((Integer) oa[0]);
          }
        }
      } catch (final Exception ex) {
        final String errorMsg = "Lucene error message: "
            + ex.getMessage()
            + " (for "
            + this.getClass().getSimpleName()
            + ": "
            + searchString
            + ").";
        filter.setErrorMessage(errorMsg);
        log.info(errorMsg);
      }
    } else {
      final Criteria criteria = session.createCriteria(HistoryEntry.class);
      setCacheRegion(criteria);
      criteria.add(Restrictions.eq("className", className));
      if (filter.getStartTimeOfModification() != null && filter.getStopTimeOfModification() != null) {
        criteria.add(Restrictions.between("timestamp", filter.getStartTimeOfModification(), filter.getStopTimeOfModification()));
      } else if (filter.getStartTimeOfModification() != null) {
        criteria.add(Restrictions.ge("timestamp", filter.getStartTimeOfModification()));
      } else if (filter.getStopTimeOfModification() != null) {
        criteria.add(Restrictions.le("timestamp", filter.getStopTimeOfModification()));
      }
      if (filter.getModifiedByUserId() != null) {
        criteria.add(Restrictions.eq("userName", filter.getModifiedByUserId().toString()));
      }
      criteria.setCacheable(true);
      criteria.setCacheRegion("historyItemCache");
      criteria.setProjection(Projections.property("entityId"));
      final List<Integer> idList = criteria.list();
      if (idList != null && idList.size() > 0) {
        for (final Integer id : idList) {
          idSet.add(id);
        }
      }
    }
  }

  protected Class< ? >[] getAdditionalHistorySearchDOs()
  {
    return null;
  }

  /**
   * @return The type of the data object (BaseDO) this dao is responsible for.
   */
  public Class< ? > getDataObjectType()
  {
    return clazz;
  }

  /**
   * @return Wether the data object (BaseDO) this dao is responsible for is from type Historizable or not.
   */
  public boolean isHistorizable()
  {
    return Historizable.class.isAssignableFrom(clazz);
  }

  /**
   * If true then a eh cache region is used for this dao for every criteria search of this class. <br/>
   * Please note: If you write your own criteria searches in extended classes, don't forget to call {@link #setCacheRegion(Criteria)}. <br/>
   * Don't forget to add your base dao class name in ehcache.xml.
   * @return false at default.
   */
  protected boolean useOwnCriteriaCacheRegion()
  {
    return false;
  }

  private void setCacheRegion(final Criteria criteria)
  {
    criteria.setCacheable(true);
    if (useOwnCriteriaCacheRegion() == false) {
      return;
    }
    criteria.setCacheRegion(this.getClass().getName());
  }
}