/////////////////////////////////////////////////////////////////////////////
//
// 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.task;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
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.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.continuousdb.DatabaseSupport;
import org.projectforge.core.BaseDao;
import org.projectforge.core.BaseSearchFilter;
import org.projectforge.core.ModificationStatus;
import org.projectforge.core.QueryFilter;
import org.projectforge.core.UserException;
import org.projectforge.fibu.ProjektDO;
import org.projectforge.user.PFUserDO;
import org.projectforge.user.ProjectForgeGroup;
import org.projectforge.user.UserDao;
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 TaskDao extends BaseDao<TaskDO>
{
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(TaskDao.class);
private static final String[] ADDITIONAL_SEARCH_FIELDS = new String[] { "responsibleUser.username", "responsibleUser.firstname",
"responsibleUser.lastname", "taskpath", "projekt.name", "projekt.kunde.name", "kost2.nummer", "kost2.description"};
public static final String I18N_KEY_ERROR_CYCLIC_REFERENCE = "task.error.cyclicReference";
public static final String I18N_KEY_ERROR_PARENT_TASK_NOT_FOUND = "task.error.parentTaskNotFound";
public static final String I18N_KEY_ERROR_PARENT_TASK_NOT_GIVEN = "task.error.parentTaskNotGiven";
public static final String I18N_KEY_ERROR_DUPLICATE_CHILD_TASKS = "task.error.duplicateChildTasks";
private TaskTree taskTree;
private UserDao userDao;
public TaskDao()
{
super(TaskDO.class);
}
public TaskTree getTaskTree()
{
return taskTree;
}
public void setTaskTree(final TaskTree taskTree)
{
this.taskTree = taskTree;
}
public void setUserDao(final UserDao userDao)
{
this.userDao = userDao;
}
@Override
protected String[] getAdditionalSearchFields()
{
return ADDITIONAL_SEARCH_FIELDS;
}
/**
* Checks constraint violation.
* @see org.projectforge.core.BaseDao#onSaveOrModify(org.projectforge.core.ExtendedBaseDO)
*/
@Override
protected void onSaveOrModify(final TaskDO obj)
{
synchronized (this) {
checkConstraintVioloation(obj);
}
}
/**
* @param task
* @param parentTaskId If null, then task will be set to null;
* @see BaseDao#getOrLoad(Integer)
*/
public TaskDO setParentTask(final TaskDO task, final Integer parentTaskId)
{
final TaskDO parentTask = getOrLoad(parentTaskId);
task.setParentTask(parentTask);
return task;
}
/**
* @param task
* @param predecessorId If null, then task will be set to null;
* @see BaseDao#getOrLoad(Integer)
*/
public void setGanttPredecessor(final TaskDO task, final Integer predecessorId)
{
final TaskDO predecessor = getOrLoad(predecessorId);
task.setGanttPredecessor(predecessor);
}
/**
* @param task
* @param responsibleUserId If null, then task will be set to null;
* @see BaseDao#getOrLoad(Integer)
*/
public void setResponsibleUser(final TaskDO task, final Integer responsibleUserId)
{
final PFUserDO user = userDao.getOrLoad(responsibleUserId);
task.setResponsibleUser(user);
}
/**
* Gets the total duration of all time sheets of all tasks (excluding the child tasks).
* @param node
* @return
*/
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List<Object[]> readTotalDurations()
{
log.debug("Calculating duration for all tasks");
final String intervalInSeconds = DatabaseSupport.getInstance().getIntervalInSeconds("startTime", "stopTime");
if (intervalInSeconds != null) {
@SuppressWarnings("unchecked")
final List<Object[]> list = getHibernateTemplate().find(
"select " + intervalInSeconds + ", task.id from TimesheetDO where deleted=false group by task.id");
return list;
}
@SuppressWarnings("unchecked")
final List<Object[]> result = getHibernateTemplate().find(
"select startTime, stopTime, task.id from TimesheetDO where deleted=false order by task.id");
final List<Object[]> list = new ArrayList<Object[]>();
if (CollectionUtils.isEmpty(result) == false) {
Integer currentTaskId = null;
long totalDuration = 0;
for (final Object[] oa : result) {
final Timestamp startTime = (Timestamp) oa[0];
final Timestamp stopTime = (Timestamp) oa[1];
final Integer taskId = (Integer) oa[2];
final long duration = (stopTime.getTime() - startTime.getTime()) / 1000;
if (currentTaskId == null || currentTaskId.equals(taskId) == false) {
if (currentTaskId != null) {
list.add(new Object[] { totalDuration, currentTaskId});
}
// New row.
currentTaskId = taskId;
totalDuration = 0;
}
totalDuration += duration;
}
if (currentTaskId != null) {
list.add(new Object[] { totalDuration, currentTaskId});
}
}
return list;
}
/**
* Gets the total duration of all time sheets of the given task (excluding the child tasks).
* @param node
* @return
*/
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public long readTotalDuration(final Integer taskId)
{
log.debug("Calculating duration for all tasks");
final String intervalInSeconds = DatabaseSupport.getInstance().getIntervalInSeconds("startTime", "stopTime");
if (intervalInSeconds != null) {
@SuppressWarnings("unchecked")
final List<Object> list = getHibernateTemplate().find(
"select "
+ DatabaseSupport.getInstance().getIntervalInSeconds("startTime", "stopTime")
+ " from TimesheetDO where task.id = ? and deleted=false", taskId);
if (list.size() == 0) {
return new Long(0);
}
Validate.isTrue(list.size() == 1);
if (list.get(0) == null) { // Has happened one time, why (PROJECTFORGE-543)?
return new Long(0);
} else if (list.get(0) instanceof Integer) {
return new Long((Integer) list.get(0));
} else {
return (Long) list.get(0);
}
}
@SuppressWarnings("unchecked")
final List<Object[]> result = getHibernateTemplate().find(
"select startTime, stopTime from TimesheetDO where task.id = ? and deleted=false", taskId);
if (CollectionUtils.isEmpty(result) == true) {
return new Long(0);
}
long totalDuration = 0;
for (final Object[] oa : result) {
final Timestamp startTime = (Timestamp) oa[0];
final Timestamp stopTime = (Timestamp) oa[1];
final long duration = stopTime.getTime() - startTime.getTime();
totalDuration += duration;
}
return totalDuration / 1000;
}
@Override
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List<TaskDO> getList(final BaseSearchFilter filter) throws AccessException
{
final TaskFilter myFilter;
if (filter instanceof TaskFilter) {
myFilter = (TaskFilter) filter;
} else {
myFilter = new TaskFilter(filter);
}
final QueryFilter queryFilter = new QueryFilter(myFilter);
final Collection<TaskStatus> col = new ArrayList<TaskStatus>(4);
if (myFilter.isNotOpened() == true) {
col.add(TaskStatus.N);
}
if (myFilter.isOpened() == true) {
col.add(TaskStatus.O);
}
if (myFilter.isClosed() == true) {
col.add(TaskStatus.C);
}
if (col.size() > 0) {
queryFilter.add(Restrictions.in("status", col));
} else {
// Note: Result set should be empty, because every task should has one of the following status values.
queryFilter.add(Restrictions.not(Restrictions.in("status", new TaskStatus[] { TaskStatus.N, TaskStatus.O, TaskStatus.C})));
}
queryFilter.addOrder(Order.asc("title"));
if (log.isDebugEnabled() == true) {
log.debug(myFilter.toString());
}
return getList(queryFilter);
}
/**
* Checks if the given task has already a sister task with the same title.
* @param task
* @throws UserException
*/
@SuppressWarnings("unchecked")
public void checkConstraintVioloation(final TaskDO task) throws UserException
{
if (task.getParentTaskId() == null) {
// Root task or task without parent task.
if (taskTree.isRootNode(task) == false) {
// Task is not root task!
throw new UserException(I18N_KEY_ERROR_PARENT_TASK_NOT_GIVEN);
}
} else {
List<TaskDO> list;
if (task.getId() != null) {
list = getHibernateTemplate().find("from TaskDO t where t.parentTask.id = ? and t.title = ? and t.id != ?",
new Object[] { task.getParentTaskId(), task.getTitle(), task.getId()});
} else {
list = getHibernateTemplate().find("from TaskDO t where t.parentTask.id = ? and t.title = ?",
new Object[] { task.getParentTaskId(), task.getTitle()});
}
if (CollectionUtils.isNotEmpty(list) == true) {
throw new UserException(I18N_KEY_ERROR_DUPLICATE_CHILD_TASKS);
}
}
}
@Override
protected void afterSaveOrModify(final TaskDO obj)
{
// Reread it from the database to get the current version (given obj could be different, for example after markAsDeleted):
final TaskDO task = internalGetById(obj.getId());
taskTree.addOrUpdateTaskNode(task);
}
/**
* Must be visible for TaskTree.
* @see org.projectforge.core.BaseDao#hasSelectAccess(java.lang.Object, boolean)
*/
@Override
public boolean hasSelectAccess(final PFUserDO user, final TaskDO obj, final boolean throwException)
{
if (accessChecker.isUserMemberOfGroup(user, false, ProjectForgeGroup.ADMIN_GROUP, ProjectForgeGroup.FINANCE_GROUP,
ProjectForgeGroup.CONTROLLING_GROUP) == true) {
return true;
}
return super.hasSelectAccess(user, obj, throwException);
}
@Override
public boolean hasSelectAccess(final PFUserDO user, final boolean throwException)
{
return true;
}
/**
* @see org.projectforge.core.BaseDao#hasAccess(Object, OperationType)
*/
@Override
public boolean hasAccess(final PFUserDO user, final TaskDO obj, final TaskDO oldObj, final OperationType operationType,
final boolean throwException)
{
return accessChecker.hasPermission(user, obj.getId(), AccessType.TASKS, operationType, throwException);
}
/**
* @see org.projectforge.core.BaseDao#hasUpdateAccess(java.lang.Object, java.lang.Object)
*/
@Override
public boolean hasUpdateAccess(final PFUserDO user, final TaskDO obj, final TaskDO dbObj, final boolean throwException)
{
Validate.notNull(dbObj);
Validate.notNull(obj);
if (taskTree.isRootNode(obj) == true) {
if (obj.getParentTaskId() != null) {
throw new UserException(TaskDao.I18N_KEY_ERROR_CYCLIC_REFERENCE);
}
if (accessChecker.isUserMemberOfGroup(user, throwException, ProjectForgeGroup.ADMIN_GROUP, ProjectForgeGroup.FINANCE_GROUP) == false) {
return false;
}
return true;
}
Validate.notNull(dbObj.getParentTaskId());
if (obj.getParentTaskId() == null) {
throw new UserException(I18N_KEY_ERROR_PARENT_TASK_NOT_GIVEN);
}
final TaskNode parent = taskTree.getTaskNodeById(obj.getParentTaskId());
if (parent == null) {
throw new UserException(I18N_KEY_ERROR_PARENT_TASK_NOT_FOUND);
}
// Checks cyclic and self reference. The parent task is not allowed to be a self reference.
checkCyclicReference(obj);
if (accessChecker.isUserMemberOfGroup(user, ProjectForgeGroup.ADMIN_GROUP, ProjectForgeGroup.FINANCE_GROUP) == true) {
return true;
}
if (accessChecker.hasPermission(user, obj.getId(), AccessType.TASKS, OperationType.UPDATE, throwException) == false) {
return false;
}
if (dbObj.getParentTaskId().equals(obj.getParentTaskId()) == false) {
// User moves the object to another task:
if (hasInsertAccess(user, obj, throwException) == false) {
// Inserting of object under new task not allowed.
return false;
}
if (accessChecker.hasPermission(user, dbObj.getParentTaskId(), AccessType.TASKS, OperationType.DELETE, throwException) == false) {
// Deleting of object under old task not allowed.
return false;
}
}
return true;
}
public boolean hasAccessForKost2AndTimesheetBookingStatus(final PFUserDO user, final TaskDO obj)
{
if (accessChecker.isUserMemberOfGroup(user, ProjectForgeGroup.FINANCE_GROUP) == true) {
return true;
}
if (obj == null) {
return false;
}
final Integer taskId = obj.getId() != null ? obj.getId() : obj.getParentTaskId();
final ProjektDO projekt = taskTree.getProjekt(taskId);
// Parent task because id of current task is null and project can't be found.
if (projekt != null && userGroupCache.isUserProjectManagerOrAssistantForProject(projekt) == true) {
return true;
}
return false;
}
@Override
protected void checkInsertAccess(final PFUserDO user, final TaskDO obj) throws AccessException
{
super.checkInsertAccess(user, obj);
if (accessChecker.isUserMemberOfGroup(user, ProjectForgeGroup.FINANCE_GROUP) == false) {
if (obj.getProtectTimesheetsUntil() != null) {
throw new AccessException("task.error.protectTimesheetsUntilReadonly");
}
if (obj.isProtectionOfPrivacy() == true) {
throw new AccessException("task.error.protectionOfPrivacyReadonly");
}
}
if (hasAccessForKost2AndTimesheetBookingStatus(user, obj) == false) {
// Non project managers are not able to manipulate the following fields:
if (StringUtils.isNotBlank(obj.getKost2BlackWhiteList()) == true || obj.isKost2IsBlackList() == true) {
throw new AccessException("task.error.kost2Readonly");
}
if (obj.getTimesheetBookingStatus() != TimesheetBookingStatus.DEFAULT) {
throw new AccessException("task.error.timesheetBookingStatus2Readonly");
}
}
}
@Override
protected void checkUpdateAccess(final PFUserDO user, final TaskDO obj, final TaskDO dbObj) throws AccessException
{
super.checkUpdateAccess(user, obj, dbObj);
if (accessChecker.isUserMemberOfGroup(user, ProjectForgeGroup.FINANCE_GROUP) == false) {
Long ts1 = null, ts2 = null;
if (obj.getProtectTimesheetsUntil() != null) {
ts1 = obj.getProtectTimesheetsUntil().getTime();
}
if (dbObj.getProtectTimesheetsUntil() != null) {
ts2 = dbObj.getProtectTimesheetsUntil().getTime();
}
if (ObjectUtils.equals(ts1, ts2) == false) {
throw new AccessException("task.error.protectTimesheetsUntilReadonly");
}
if (ObjectUtils.equals(obj.isProtectionOfPrivacy(), dbObj.isProtectionOfPrivacy()) == false) {
throw new AccessException("task.error.protectionOfPrivacyReadonly");
}
}
if (hasAccessForKost2AndTimesheetBookingStatus(user, obj) == false) {
// Non project managers are not able to manipulate the following fields:
if (ObjectUtils.equals(obj.getKost2BlackWhiteList(), dbObj.getKost2BlackWhiteList()) == false
|| obj.isKost2IsBlackList() != dbObj.isKost2IsBlackList()) {
throw new AccessException("task.error.kost2Readonly");
}
if (obj.getTimesheetBookingStatus() != dbObj.getTimesheetBookingStatus()) {
throw new AccessException("task.error.timesheetBookingStatus2Readonly");
}
}
}
@Override
public boolean hasInsertAccess(final PFUserDO user, final TaskDO obj, final boolean throwException)
{
Validate.notNull(obj);
// Checks if the task is orphan.
final TaskNode parent = taskTree.getTaskNodeById(obj.getParentTaskId());
if (parent == null) {
if (taskTree.isRootNode(obj) == true && obj.isDeleted() == true) {
// Oups, the user has deleted the root task!
} else {
throw new UserException(I18N_KEY_ERROR_PARENT_TASK_NOT_FOUND);
}
}
if (accessChecker.isUserMemberOfGroup(user, ProjectForgeGroup.ADMIN_GROUP, ProjectForgeGroup.FINANCE_GROUP) == true) {
return true;
}
return accessChecker.hasPermission(user, obj.getParentTaskId(), AccessType.TASKS, OperationType.INSERT, throwException);
}
@Override
public boolean hasDeleteAccess(final PFUserDO user, final TaskDO obj, final TaskDO dbObj, final boolean throwException)
{
Validate.notNull(obj);
if (hasUpdateAccess(user, obj, dbObj, throwException) == true) {
return true;
}
if (accessChecker.isUserMemberOfGroup(user, ProjectForgeGroup.ADMIN_GROUP, ProjectForgeGroup.FINANCE_GROUP) == true) {
return true;
}
return accessChecker.hasPermission(user, obj.getParentTaskId(), AccessType.TASKS, OperationType.DELETE, throwException);
}
@Override
protected ModificationStatus copyValues(final TaskDO src, final TaskDO dest, final String... ignoreFields)
{
ModificationStatus modified = super.copyValues(src, dest, ignoreFields);
// Priority value is null-able (may be was not copied from super.copyValues):
if (ObjectUtils.equals(dest.getPriority(), src.getPriority()) == false) {
dest.setPriority(src.getPriority());
modified = ModificationStatus.MAJOR;
}
// User object is null-able:
if (src.getResponsibleUser() == null) {
if (dest.getResponsibleUser() != null) {
dest.setResponsibleUser(src.getResponsibleUser());
modified = ModificationStatus.MAJOR;
}
}
return modified;
}
private void checkCyclicReference(final TaskDO obj)
{
if (obj.getId().equals(obj.getParentTaskId()) == true) {
// Self reference
throw new UserException(I18N_KEY_ERROR_CYCLIC_REFERENCE);
}
final TaskNode parent = taskTree.getTaskNodeById(obj.getParentTaskId());
if (parent == null) {
// Task is orphan because it has no parent task.
throw new UserException(I18N_KEY_ERROR_PARENT_TASK_NOT_FOUND);
}
final TaskNode node = taskTree.getTaskNodeById(obj.getId());
if (node.isParentOf(parent) == true) {
// Cyclic reference because task is ancestor of itself.
throw new UserException(TaskDao.I18N_KEY_ERROR_CYCLIC_REFERENCE);
}
}
/**
* Checks only root task (can't be deleted).
* @see org.projectforge.core.BaseDao#onDelete(org.projectforge.core.ExtendedBaseDO)
*/
@Override
protected void onDelete(final TaskDO obj)
{
if (taskTree.isRootNode(obj) == true) {
throw new UserException("task.error.couldNotDeleteRootTask");
}
}
/**
* Re-index all dependent objects only if the title was changed.
* @see org.projectforge.core.BaseDao#wantsReindexAllDependentObjects(org.projectforge.core.ExtendedBaseDO,
* org.projectforge.core.ExtendedBaseDO)
*/
@Override
protected boolean wantsReindexAllDependentObjects(final TaskDO obj, final TaskDO dbObj)
{
if (super.wantsReindexAllDependentObjects(obj, dbObj) == false) {
return false;
}
return StringUtils.equals(obj.getTitle(), dbObj.getTitle()) == false;
}
@Override
public TaskDO newInstance()
{
return new TaskDO();
}
/**
* @see org.projectforge.core.BaseDao#useOwnCriteriaCacheRegion()
*/
@Override
protected boolean useOwnCriteriaCacheRegion()
{
return true;
}
}