org.projectforge.timesheet.TimesheetDao.java Source code

Java tutorial

Introduction

Here is the source code for org.projectforge.timesheet.TimesheetDao.java

Source

/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
//         www.projectforge.org
//
// Copyright (C) 2001-2013 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.timesheet;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.log4j.Logger;
import org.hibernate.Hibernate;
import org.hibernate.Query;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.projectforge.access.AccessException;
import org.projectforge.access.AccessType;
import org.projectforge.access.OperationType;
import org.projectforge.common.DateHelper;
import org.projectforge.common.DateHolder;
import org.projectforge.common.NumberHelper;
import org.projectforge.core.BaseDao;
import org.projectforge.core.BaseSearchFilter;
import org.projectforge.core.MessageParam;
import org.projectforge.core.OrderDirection;
import org.projectforge.core.QueryFilter;
import org.projectforge.core.UserException;
import org.projectforge.database.SQLHelper;
import org.projectforge.fibu.kost.Kost2DO;
import org.projectforge.fibu.kost.Kost2Dao;
import org.projectforge.task.TaskDO;
import org.projectforge.task.TaskNode;
import org.projectforge.task.TaskStatus;
import org.projectforge.task.TaskTree;
import org.projectforge.task.TimesheetBookingStatus;
import org.projectforge.user.PFUserContext;
import org.projectforge.user.PFUserDO;
import org.projectforge.user.ProjectForgeGroup;
import org.projectforge.user.UserDao;
import org.projectforge.web.timesheet.TimesheetListFilter;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * 
 * @author Kai Reinhard (k.reinhard@micromata.de)
 * 
 */
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public class TimesheetDao extends BaseDao<TimesheetDO> {
    /**
     * Maximum allowed duration of time sheets is 14 hours.
     */
    public static final long MAXIMUM_DURATION = 1000 * 3600 * 14;

    /**
     * Internal error message if maximum duration is exceeded.
     */
    private static final String MAXIMUM_DURATION_EXCEEDED = "Maximum duration of time sheet exceeded. Maximum is "
            + (MAXIMUM_DURATION / 3600 / 1000) + "h!";

    private static final String[] ADDITIONAL_SEARCH_FIELDS = new String[] { "user.username", "user.firstname",
            "user.lastname", "task.title", "task.taskpath", "kost2.nummer", "kost2.description",
            "kost2.projekt.name" };

    public static final String HIDDEN_FIELD_MARKER = "[...]";

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

    private TaskTree taskTree;

    private UserDao userDao;

    private Kost2Dao kost2Dao;

    private final Map<Integer, Set<Integer>> timesheetsWithOverlapByUser = new HashMap<Integer, Set<Integer>>();

    public void setTaskTree(final TaskTree taskTree) {
        this.taskTree = taskTree;
    }

    public void setUserDao(final UserDao userDao) {
        this.userDao = userDao;
    }

    public void setKost2Dao(final Kost2Dao kost2Dao) {
        this.kost2Dao = kost2Dao;
    }

    @Override
    protected String[] getAdditionalSearchFields() {
        return ADDITIONAL_SEARCH_FIELDS;
    }

    /**
     * List of all years with time sheets of the given user: select min(startTime), max(startTime) from t_timesheet where user=?.
     * @return
     */
    @SuppressWarnings("unchecked")
    public int[] getYears(final Integer userId) {
        final List<Object[]> list = getHibernateTemplate().find(
                "select min(startTime), max(startTime) from TimesheetDO t where user.id=? and deleted=false",
                userId);
        return SQLHelper.getYears(list);
    }

    /**
     * @param sheet
     * @param userId If null, then task will be set to null;
     * @see BaseDao#getOrLoad(Integer)
     */
    public void setUser(final TimesheetDO sheet, final Integer userId) {
        final PFUserDO user = userDao.getOrLoad(userId);
        sheet.setUser(user);
    }

    /**
     * @param sheet
     * @param taskId If null, then task will be set to null;
     * @see TaskTree#getTaskById(Integer)
     */
    public void setTask(final TimesheetDO sheet, final Integer taskId) {
        final TaskDO task = taskTree.getTaskById(taskId);
        sheet.setTask(task);
    }

    /**
     * @param sheet
     * @param kost2Id If null, then kost2 will be set to null;
     * @see BaseDao#getOrLoad(Integer)
     */
    public void setKost2(final TimesheetDO sheet, final Integer kost2Id) {
        final Kost2DO kost2 = kost2Dao.getOrLoad(kost2Id);
        sheet.setKost2(kost2);
    }

    /**
     * Gets the available Kost2DO's for the given time sheet. The task must already be assigned to this time sheet.
     * @param timesheet
     * @return Available list of Kost2DO's or null, if not exist.
     */
    public List<Kost2DO> getKost2List(final TimesheetDO timesheet) {
        if (timesheet == null || timesheet.getTaskId() == null) {
            return null;
        }
        return taskTree.getKost2List(timesheet.getTaskId());
    }

    public QueryFilter buildQueryFilter(final TimesheetFilter filter) {
        final QueryFilter queryFilter = new QueryFilter(filter);
        if (filter.getUserId() != null) {
            final PFUserDO user = new PFUserDO();
            user.setId(filter.getUserId());
            queryFilter.add(Restrictions.eq("user", user));
        }
        if (filter.getStartTime() != null && filter.getStopTime() != null) {
            queryFilter.add(Restrictions.between("startTime", filter.getStartTime(), filter.getStopTime()));
        } else if (filter.getStartTime() != null) {
            queryFilter.add(Restrictions.ge("startTime", filter.getStartTime()));
        } else if (filter.getStopTime() != null) {
            queryFilter.add(Restrictions.le("startTime", filter.getStopTime()));
        }
        if (filter.getTaskId() != null) {
            if (filter.isRecursive() == true) {
                final TaskNode node = taskTree.getTaskNodeById(filter.getTaskId());
                final List<Integer> taskIds = node.getDescendantIds();
                taskIds.add(node.getId());
                queryFilter.add(Restrictions.in("task.id", taskIds));
                if (log.isDebugEnabled() == true) {
                    log.debug("search in tasks: " + taskIds);
                }
            } else {
                queryFilter.add(Restrictions.eq("task.id", filter.getTaskId()));
            }
        }
        if (filter.getOrderType() == OrderDirection.DESC) {
            queryFilter.addOrder(Order.desc("startTime"));
        } else {
            queryFilter.addOrder(Order.asc("startTime"));
        }
        if (log.isDebugEnabled() == true) {
            log.debug(ToStringBuilder.reflectionToString(filter));
        }
        return queryFilter;
    }

    public TimesheetDao() {
        super(TimesheetDO.class);
    }

    /**
     * @see org.projectforge.core.BaseDao#getListForSearchDao(org.projectforge.core.BaseSearchFilter)
     */
    @Override
    public List<TimesheetDO> getListForSearchDao(final BaseSearchFilter filter) {
        final TimesheetFilter timesheetFilter = new TimesheetFilter(filter);
        if (filter.getModifiedByUserId() == null) {
            timesheetFilter.setUserId(PFUserContext.getUserId());
        }
        return getList(timesheetFilter);
    }

    /**
     * Gets the list filtered by the given filter.
     * @param filter
     * @return
     */
    @Override
    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    public List<TimesheetDO> getList(final BaseSearchFilter filter) throws AccessException {
        final TimesheetFilter myFilter;
        if (filter instanceof TimesheetFilter) {
            myFilter = (TimesheetFilter) filter;
        } else {
            myFilter = new TimesheetFilter(filter);
        }
        if (myFilter.getStopTime() != null) {
            final DateHolder date = new DateHolder(myFilter.getStopTime());
            date.setEndOfDay();
            myFilter.setStopTime(date.getDate());
        }
        final QueryFilter queryFilter = buildQueryFilter(myFilter);
        List<TimesheetDO> result = getList(queryFilter);
        if (result == null) {
            return null;
        }
        // Check time period overlaps:
        for (final TimesheetDO entry : result) {
            Validate.notNull(entry.getUserId());
            if (entry.isMarked() == true) {
                continue; // Is already marked.
            }
            final Set<Integer> overlapSet = getTimesheetsWithTimeoverlap(entry.getUserId());
            if (overlapSet.contains(entry.getId()) == true) {
                log.info("Overlap of time sheet decteced: " + entry);
                entry.setMarked(true);
            }
        }
        if (myFilter.isMarked() == true) {
            // Show only time sheets with time period violation (overlap):
            final List<TimesheetDO> list = result;
            result = new ArrayList<TimesheetDO>();
            for (final TimesheetDO entry : list) {
                if (entry.isMarked() == true) {
                    result.add(entry);
                }
            }
        }
        return result;
    }

    public List<TimesheetDO> getTimeperiodOverlapList(final TimesheetListFilter actionFilter) {
        if (actionFilter.getUserId() != null) {
            final QueryFilter queryFilter = new QueryFilter(actionFilter);
            final Set<Integer> set = getTimesheetsWithTimeoverlap(actionFilter.getUserId());
            if (set == null || set.size() == 0) {
                // No time sheets with overlap found.
                return new ArrayList<TimesheetDO>();
            }
            queryFilter.add(Restrictions.in("id", set));
            final List<TimesheetDO> result = getList(queryFilter);
            for (final TimesheetDO entry : result) {
                entry.setMarked(true);
            }
            Collections.sort(result, Collections.reverseOrder());
            return result;
        }
        return getList(actionFilter);
    }

    /**
     * Rechecks the time sheet overlaps.
     * @see org.projectforge.core.BaseDao#afterSaveOrModify(org.projectforge.core.ExtendedBaseDO)
     */
    @Override
    protected void afterSaveOrModify(final TimesheetDO obj) {
        super.afterSaveOrModify(obj);
        if (obj.getUser() != null) {
            // Force re-analysis of time sheet overlaps after any modification of time sheets.
            recheckTimesheetOverlap(obj.getUserId());
        }
        taskTree.resetTotalDuration(obj.getTaskId());
    }

    /**
     * Checks the start and stop time. If seconds or millis is not null, a RuntimeException will be thrown.
     * @see org.projectforge.core.BaseDao#onSaveOrModify(org.projectforge.core.ExtendedBaseDO)
     */
    @Override
    protected void onSaveOrModify(final TimesheetDO obj) {
        validateTimestamp(obj.getStartTime(), "startTime");
        validateTimestamp(obj.getStopTime(), "stopTime");
        Validate.isTrue(obj.getDuration() >= 60000, "Duration of time sheet must be at minimum 60s!");
        Validate.isTrue(obj.getDuration() <= MAXIMUM_DURATION, MAXIMUM_DURATION_EXCEEDED);
        Validate.isTrue(obj.getStartTime().before(obj.getStopTime()),
                "Stop time of time sheet is before start time!");
        final List<Kost2DO> kost2List = taskTree.getKost2List(obj.getTaskId());
        final Integer kost2Id = obj.getKost2Id();
        if (CollectionUtils.isNotEmpty(kost2List) == true) {
            Validate.notNull(kost2Id, "Kost2Id must be given for time sheet and given kost2 list!");
            boolean kost2IdFound = false;
            for (final Kost2DO kost2 : kost2List) {
                if (NumberHelper.isEqual(kost2Id, kost2.getId()) == true) {
                    kost2IdFound = true;
                    break;
                }
            }
            Validate.isTrue(kost2IdFound, "Kost2Id of time sheet is not available in the task's kost2 list!");
        } else {
            Validate.isTrue(kost2Id == null, "Kost2Id can't be given for task without any kost2 entries!");
        }
    }

    @Override
    protected void onChange(final TimesheetDO obj, final TimesheetDO dbObj) {
        if (obj.getTaskId().compareTo(dbObj.getTaskId()) != 0) {
            taskTree.resetTotalDuration(dbObj.getTaskId());
        }
    }

    /**
     * @see org.projectforge.core.BaseDao#prepareHibernateSearch(org.projectforge.core.ExtendedBaseDO, org.projectforge.access.OperationType)
     */
    @Override
    protected void prepareHibernateSearch(final TimesheetDO obj, final OperationType operationType) {
        final PFUserDO user = obj.getUser();
        if (user != null && Hibernate.isInitialized(user) == false) {
            obj.setUser(userDao.getUserGroupCache().getUser(user.getId()));
        }
        final TaskDO task = obj.getTask();
        if (task != null && Hibernate.isInitialized(task) == false) {
            obj.setTask(taskTree.getTaskById(task.getId()));
        }
    }

    private void validateTimestamp(final Date date, final String name) {
        if (date == null) {
            return;
        }
        final Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        Validate.isTrue(cal.get(Calendar.MILLISECOND) == 0, "Millis of " + name + " is not 0!");
        Validate.isTrue(cal.get(Calendar.SECOND) == 0, "Seconds of " + name + " is not 0!");
        final int m = cal.get(Calendar.MINUTE);
        Validate.isTrue(m == 0 || m == 15 || m == 30 || m == 45, "Minutes of " + name + " must be 0, 15, 30 or 45");
    }

    /**
     * Analyses all time sheets of the user and detects any collision (overlap) of the user's time sheets. The result will be cached and the
     * duration of a new analysis is only a few milliseconds!
     * @param user
     * @return
     */
    public Set<Integer> getTimesheetsWithTimeoverlap(final Integer userId) {
        Validate.notNull(userId);
        final PFUserDO user = userGroupCache.getUser(userId);
        Validate.notNull(user);
        synchronized (timesheetsWithOverlapByUser) {
            if (timesheetsWithOverlapByUser.get(userId) != null) {
                return timesheetsWithOverlapByUser.get((userId));
            }
            // log.info("Getting time sheet overlaps for user: " + user.getUsername());
            final Set<Integer> result = new HashSet<Integer>();
            final QueryFilter queryFilter = new QueryFilter();
            queryFilter.add(Restrictions.eq("user", user));
            queryFilter.addOrder(Order.asc("startTime"));
            final List<TimesheetDO> list = getList(queryFilter);
            long endTime = 0;
            TimesheetDO lastEntry = null;
            for (final TimesheetDO entry : list) {
                if (entry.getStartTime().getTime() < endTime) {
                    // Time collision!
                    result.add(entry.getId());
                    if (lastEntry != null) { // Only for first iteration
                        result.add(lastEntry.getId()); // Also collision for last entry.
                    }
                }
                endTime = entry.getStopTime().getTime();
                lastEntry = entry;
            }
            timesheetsWithOverlapByUser.put(user.getId(), result);
            if (CollectionUtils.isNotEmpty(result) == true) {
                log.info("Time sheet overlaps for user '" + user.getUsername() + "': " + result);
            }
            return result;
        }
    }

    /**
     * Deletes any existing time sheet overlap analysis and forces therefore a new analysis before next time sheet list selection. (The
     * analysis will not be started inside this method!)
     * @param userId
     */
    public void recheckTimesheetOverlap(final Integer userId) {
        Validate.notNull(userId);
        timesheetsWithOverlapByUser.remove(userId);
    }

    /**
     * Checks if the time sheet overlaps with another time sheet of the same user. Should be checked on every insert or update (also
     * undelete). For time collision detection deleted time sheets are ignored.
     * @return The existing time sheet with the time period collision.
     */
    public boolean hasTimeOverlap(final TimesheetDO timesheet, final boolean throwException) {
        Validate.notNull(timesheet);
        Validate.notNull(timesheet.getUser());
        final QueryFilter queryFilter = new QueryFilter();
        queryFilter.add(Restrictions.eq("user", timesheet.getUser()));
        queryFilter.add(Restrictions.lt("startTime", timesheet.getStopTime()));
        queryFilter.add(Restrictions.gt("stopTime", timesheet.getStartTime()));
        if (timesheet.getId() != null) {
            // Update time sheet, do not compare with itself.
            queryFilter.add(Restrictions.ne("id", timesheet.getId()));
        }
        final List<TimesheetDO> list = getList(queryFilter);
        if (list != null && list.size() > 0) {
            final TimesheetDO ts = list.get(0);
            if (throwException == true) {
                log.info("Time sheet collision detected of time sheet " + timesheet + " with existing time sheet "
                        + ts);
                final String startTime = DateHelper.formatIsoTimestamp(ts.getStartTime());
                final String stopTime = DateHelper.formatIsoTimestamp(ts.getStopTime());
                throw new UserException("timesheet.error.timeperiodOverlapDetection", new MessageParam(ts.getId()),
                        new MessageParam(startTime), new MessageParam(stopTime));
            }
            return true;
        }
        return false;
    }

    /**
     * return Always true, no generic select access needed for address objects.
     * @see org.projectforge.core.BaseDao#hasSelectAccess()
     */
    @Override
    public boolean hasSelectAccess(final PFUserDO user, final boolean throwException) {
        return true;
    }

    @Override
    public boolean hasAccess(final PFUserDO user, final TimesheetDO obj, final TimesheetDO oldObj,
            final OperationType operationType, final boolean throwException) {
        if (accessChecker.userEquals(user, obj.getUser()) == true) {
            // Own time sheet
            if (accessChecker.hasPermission(user, obj.getTaskId(), AccessType.OWN_TIMESHEETS, operationType,
                    throwException) == false) {
                return false;
            }
        } else {
            // Foreign time sheet
            if (accessChecker.isUserMemberOfGroup(user, ProjectForgeGroup.FINANCE_GROUP) == true) {
                return true;
            }
            if (accessChecker.hasPermission(user, obj.getTaskId(), AccessType.TIMESHEETS, operationType,
                    throwException) == false) {
                return false;
            }
        }
        if (operationType == OperationType.DELETE) {
            // UPDATE and INSERT is already checked, SELECT will be ignored.
            final boolean result = checkTimesheetProtection(user, obj, null, operationType, throwException);
            return result;
        }
        return true;
    }

    /**
     * User can always see his own time sheets. But if he has no access then the location and description values are hidden (empty strings).
     * @see org.projectforge.core.BaseDao#hasSelectAccess(PFUserDO, org.projectforge.core.ExtendedBaseDO, boolean)
     */
    @Override
    public boolean hasSelectAccess(final PFUserDO user, final TimesheetDO obj, final boolean throwException) {
        if (hasAccess(user, obj, null, OperationType.SELECT, false) == false) {
            // User has no access by definition.
            if (accessChecker.userEquals(user, obj.getUser()) == true
                    || accessChecker.isUserMemberOfGroup(user, ProjectForgeGroup.PROJECT_MANAGER) == true) {
                if (accessChecker.userEquals(user, obj.getUser()) == false) {
                    // Check protection of privacy for foreign time sheets:
                    final List<TaskNode> pathToRoot = taskTree.getPathToRoot(obj.getTaskId());
                    for (final TaskNode node : pathToRoot) {
                        if (node.getTask().isProtectionOfPrivacy() == true) {
                            return false;
                        }
                    }
                }
                // An user should see his own time sheets, but the values should be hidden.
                // A project manager should also see all time sheets, but the values should be hidden.
                getSession().evict(obj);
                obj.setDescription(HIDDEN_FIELD_MARKER);
                obj.setLocation(HIDDEN_FIELD_MARKER);
                log.debug("User has no access to own time sheet (or project manager): " + obj);
                return true;
            }
        }
        return super.hasSelectAccess(user, obj, throwException);
    }

    @Override
    public boolean hasHistoryAccess(final PFUserDO user, final TimesheetDO obj, final boolean throwException) {
        return hasAccess(user, obj, null, OperationType.SELECT, throwException);
    }

    /**
     * @see org.projectforge.core.BaseDao#hasUpdateAccess(Object, Object)
     */
    @Override
    public boolean hasUpdateAccess(final PFUserDO user, final TimesheetDO obj, final TimesheetDO dbObj,
            final boolean throwException) {
        Validate.notNull(dbObj);
        Validate.notNull(obj);
        Validate.notNull(dbObj.getTaskId());
        Validate.notNull(obj.getTaskId());
        if (hasAccess(user, obj, dbObj, OperationType.UPDATE, throwException) == false) {
            return false;
        }
        if (dbObj.getUserId().equals(obj.getUserId()) == false) {
            // User changes the owner of the time sheet:
            if (hasAccess(user, dbObj, null, OperationType.DELETE, throwException) == false) {
                // Deleting of time sheet of another user is not allowed.
                return false;
            }
        }
        if (dbObj.getTaskId().equals(obj.getTaskId()) == false) {
            // User moves the object to another task:
            if (hasAccess(user, obj, null, OperationType.INSERT, throwException) == false) {
                // Inserting of object under new task not allowed.
                return false;
            }
            if (hasAccess(user, dbObj, null, OperationType.DELETE, throwException) == false) {
                // Deleting of object under old task not allowed.
                return false;
            }
        }
        if (hasTimeOverlap(obj, throwException) == true) {
            return false;
        }
        boolean result = checkTimesheetProtection(user, obj, dbObj, OperationType.UPDATE, throwException);
        if (result == true) {
            result = checkTaskBookable(obj, dbObj, OperationType.UPDATE, throwException);
        }
        return result;
    }

    @Override
    public boolean hasInsertAccess(final PFUserDO user, final TimesheetDO obj, final boolean throwException) {
        if (hasAccess(user, obj, null, OperationType.INSERT, throwException) == false) {
            return false;
        }
        if (hasTimeOverlap(obj, throwException) == true) {
            return false;
        }
        boolean result = checkTimesheetProtection(user, obj, null, OperationType.INSERT, throwException);
        if (result == true) {
            result = checkTaskBookable(obj, null, OperationType.INSERT, throwException);
        }
        return result;
    }

    /**
     * Checks whether the time sheet is book-able or not. The checks are:
     * <ol>
     * <li>Only for update mode: If the time sheet is unmodified in start and stop time, kost2, task and user then return true without further
     * checking.</li>
     * <li>Is the task or any of the ancestor tasks closed or deleted?</li>
     * <li>Has the task or any of the ancestor tasks the TimesheetBookingStatus.TREE_CLOSED?</li>
     * <li>Is the task not a leaf node and has this task or ancestor task the booking status ONLY_LEAFS?</li>
     * <li>Does any of the descendant task node has an assigned order position?</li>
     * </ol>
     * @param timesheet The time sheet to insert or update.
     * @param oldTimesheet The origin time sheet from the data base (could be null, if no update is done).
     * @param operationType
     * @param throwException
     * @return True if none of the rules above matches.
     */
    public boolean checkTaskBookable(final TimesheetDO timesheet, final TimesheetDO oldTimesheet,
            final OperationType operationType, final boolean throwException) {
        if (operationType == OperationType.UPDATE) {
            if (timesheet.getStartTime().getTime() == oldTimesheet.getStartTime().getTime()
                    && timesheet.getStopTime().getTime() == oldTimesheet.getStopTime().getTime()
                    && ObjectUtils.equals(timesheet.getKost2Id(), oldTimesheet.getKost2Id()) == true
                    && ObjectUtils.equals(timesheet.getTaskId(), oldTimesheet.getTaskId()) == true
                    && ObjectUtils.equals(timesheet.getUserId(), oldTimesheet.getUserId()) == true) {
                // Only minor fields are modified (description, location etc.).
                return true;
            }
        }
        final TaskNode taskNode = taskTree.getTaskNodeById(timesheet.getTaskId());
        // 1. Is the task or any of the ancestor tasks closed, deleted or has the booking status TREE_CLOSED?
        TaskNode node = taskNode;
        do {
            final TaskDO task = node.getTask();
            String errorMessage = null;
            if (task.isDeleted() == true) {
                errorMessage = "timesheet.error.taskNotBookable.taskDeleted";
            } else if (task.getStatus().isIn(TaskStatus.O, TaskStatus.N) == false) {
                errorMessage = "timesheet.error.taskNotBookable.taskNotOpened";
            } else if (task.getTimesheetBookingStatus() == TimesheetBookingStatus.TREE_CLOSED) {
                errorMessage = "timesheet.error.taskNotBookable.treeClosedForBooking";
            }
            if (errorMessage != null) {
                if (throwException == true) {
                    throw new AccessException(errorMessage, task.getTitle() + " (#" + task.getId() + ")");
                }
                return false;
            }
            node = node.getParent();
        } while (node != null);
        // 2. Has the task the booking status NO_BOOKING?
        TimesheetBookingStatus bookingStatus = taskNode.getTask().getTimesheetBookingStatus();
        node = taskNode;
        while (bookingStatus == TimesheetBookingStatus.INHERIT && node.getParent() != null) {
            node = node.getParent();
            bookingStatus = node.getTask().getTimesheetBookingStatus();
        }
        if (bookingStatus == TimesheetBookingStatus.NO_BOOKING) {
            if (throwException == true) {
                throw new AccessException("timesheet.error.taskNotBookable.taskClosedForBooking",
                        taskNode.getTask().getTitle() + " (#" + taskNode.getId() + ")");
            }
            return false;
        }
        if (taskNode.hasChilds() == true) {
            // 3. Is the task not a leaf node and has this task or ancestor task the booking status ONLY_LEAFS?
            node = taskNode;
            do {
                final TaskDO task = node.getTask();
                if (task.getTimesheetBookingStatus() == TimesheetBookingStatus.ONLY_LEAFS) {
                    if (throwException == true) {
                        throw new AccessException("timesheet.error.taskNotBookable.onlyLeafsAllowedForBooking",
                                taskNode.getTask().getTitle() + " (#" + taskNode.getId() + ")");
                    }
                    return false;
                }
                node = node.getParent();
            } while (node != null);
            // 4. Does any of the descendant task node has an assigned order position?
            for (final TaskNode child : taskNode.getChilds()) {
                if (taskTree.hasOrderPositions(child.getId(), true) == true) {
                    if (throwException == true) {
                        throw new AccessException("timesheet.error.taskNotBookable.orderPositionsFoundInSubTasks",
                                taskNode.getTask().getTitle() + " (#" + taskNode.getId() + ")");
                    }
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Checks if there exists any time sheet protection on the corresponding task or one of the ancestor tasks. If the times sheet is
     * protected and the duration of this time sheet is modified, and AccessException will be thrown. <br/>
     * Checks insert, update and delete operations. If an existing time sheet has to be modified, the check will only be done, if any
     * modifications of the time stamps is done (e. g. descriptions of the task are allowed if the start and stop time is untouched).
     * @param timesheet
     * @param oldTimesheet (null for delete and insert)
     * @param throwException If true and the time sheet protection is violated then an AccessException will be thrown.
     * @return true, if no time sheet protection is violated or if the logged in user is member of the finance group.
     * @see ProjectForgeGroup#FINANCE_GROUP
     */
    public boolean checkTimesheetProtection(final PFUserDO user, final TimesheetDO timesheet,
            final TimesheetDO oldTimesheet, final OperationType operationType, final boolean throwException) {
        if (accessChecker.isUserMemberOfGroup(user, ProjectForgeGroup.FINANCE_GROUP) == true
                && accessChecker.userEquals(user, timesheet.getUser()) == false) {
            // Member of financial group are able to book foreign time sheets.
            return true;
        }
        if (operationType == OperationType.UPDATE) {
            if (timesheet.getStartTime().getTime() == oldTimesheet.getStartTime().getTime()
                    && timesheet.getStopTime().getTime() == oldTimesheet.getStopTime().getTime()
                    && ObjectUtils.equals(timesheet.getKost2Id(), oldTimesheet.getKost2Id()) == true) {
                return true;
            }
        }
        final TaskNode taskNode = taskTree.getTaskNodeById(timesheet.getTaskId());
        Validate.notNull(taskNode);
        final List<TaskNode> list = taskNode.getPathToRoot();
        list.add(0, taskTree.getRootTaskNode());
        for (final TaskNode node : list) {
            final Date date = node.getTask().getProtectTimesheetsUntil();
            if (date == null) {
                continue;
            }
            final DateHolder dh = new DateHolder(date);
            dh.setEndOfDay();
            if (timesheet.getStartTime().before(dh.getDate()) == true) {
                if (throwException == true) {
                    throw new AccessException("timesheet.error.timesheetProtectionVioloation",
                            node.getTask().getTitle() + " (#" + node.getTaskId() + ")",
                            DateHelper.formatIsoDate(dh.getDate()));
                }
                return false;
            }
        }
        return true;
    }

    /**
     * Get all locations of the user's time sheet (not deleted ones) with modification date within last year.
     * @param searchString
     */
    @SuppressWarnings("unchecked")
    public List<String> getLocationAutocompletion(final String searchString) {
        checkLoggedInUserSelectAccess();
        if (StringUtils.isBlank(searchString) == true) {
            return null;
        }
        final String s = "select distinct location from " + clazz.getSimpleName()
                + " t where deleted=false and t.user.id = ? and lastUpdate > ? and lower(t.location) like ?) order by t.location";
        final Query query = getSession().createQuery(s);
        query.setInteger(0, PFUserContext.getUser().getId());
        final DateHolder dh = new DateHolder();
        dh.add(Calendar.YEAR, -1);
        query.setDate(1, dh.getDate());
        query.setString(2, "%" + StringUtils.lowerCase(searchString) + "%");
        final List<String> list = query.list();
        return list;
    }

    /**
     * Get all locations of the user's time sheet (not deleted ones) with modification date within last year.
     * @param maxResults Limit the result to the recent locations.
     * @return result as Json object.
     */
    @SuppressWarnings("unchecked")
    public Collection<String> getRecentLocation(final int maxResults) {
        checkLoggedInUserSelectAccess();
        log.info("Get recent locations from the database.");
        final String s = "select location from " + (clazz.getSimpleName()
                + " t where deleted=false and t.user.id = ? and lastUpdate > ? and t.location != null and t.location != '' order by t.lastUpdate desc");
        final Query query = getSession().createQuery(s);
        query.setInteger(0, PFUserContext.getUser().getId());
        final DateHolder dh = new DateHolder();
        dh.add(Calendar.YEAR, -1);
        query.setDate(1, dh.getDate());
        final List<Object> list = query.list();
        int counter = 0;
        final List<String> res = new ArrayList<String>();
        for (final Object loc : list) {
            if (res.contains(loc) == true) {
                continue;
            }
            res.add((String) loc);
            if (++counter >= maxResults) {
                break;
            }
        }
        return res;
    }

    @Override
    protected Object prepareMassUpdateStore(final List<TimesheetDO> list, final TimesheetDO master) {
        if (master.getTaskId() != null) {
            return getKost2List(master);
        }
        return null;
    }

    private boolean contains(final List<Kost2DO> kost2List, final Integer kost2Id) {
        for (final Kost2DO entry : kost2List) {
            if (kost2Id.compareTo(entry.getId()) == 0) {
                return true;
            }
        }
        return false;
    }

    @Override
    protected boolean massUpdateEntry(final TimesheetDO entry, final TimesheetDO master, final Object store) {
        if (store != null) {
            @SuppressWarnings("unchecked")
            final List<Kost2DO> kost2List = (List<Kost2DO>) store;
            if (master.getKost2Id() != null) {
                if (contains(kost2List, master.getKost2Id()) == false) {
                    log.info(
                            "Mass update not possible for time sheet (destination task does not support given kost2 id): "
                                    + entry);
                    return false;
                }
                setKost2(entry, master.getKost2Id());
            } else if (entry.getKost2Id() == null) {
                log.info("Mass update not possible for time sheet (destination task requires kost2): " + entry);
                return false;
            } else {
                if (contains(kost2List, entry.getKost2Id()) == false) {
                    // Try to convert kost2 ids from old project to new project.
                    boolean success = false;
                    for (final Kost2DO kost2 : kost2List) {
                        if (kost2.getKost2ArtId().compareTo(entry.getKost2().getKost2ArtId()) == 0) {
                            success = true; // found.
                            entry.setKost2(kost2);
                            break;
                        }
                    }
                    if (success == false) {
                        log.info(
                                "Mass update not possible for time sheet (destination task have multiple kost2 entries and no correspondent kost2 art): "
                                        + entry);
                        return false;
                    }
                }
            }
        }
        if (master.getTaskId() != null) {
            setTask(entry, master.getTaskId());
        }
        if (master.getKost2Id() != null) {
            setKost2(entry, master.getKost2Id());
        }
        if (StringUtils.isNotBlank(master.getLocation()) == true) {
            entry.setLocation(master.getLocation());
        }
        return true;
    }

    @Override
    public TimesheetDO newInstance() {
        return new TimesheetDO();
    }

    /**
     * @see org.projectforge.core.BaseDao#useOwnCriteriaCacheRegion()
     */
    @Override
    protected boolean useOwnCriteriaCacheRegion() {
        return true;
    }
}