///////////////////////////////////////////////////////////////////////////// // // Project ProjectForge Community Edition // www.projectforge.org // // Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de) // // ProjectForge is dual-licensed. // // This community edition is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 3 of the License. // // This community edition is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General // Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, see http://www.gnu.org/licenses/. // ///////////////////////////////////////////////////////////////////////////// package org.projectforge.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.and(Restrictions.ge("stopTime", filter.getStartTime()), Restrictions.le("startTime", 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; } }