///////////////////////////////////////////////////////////////////////////// // // 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.io.Serializable; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.apache.commons.lang.builder.HashCodeBuilder; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.log4j.Logger; import org.dom4j.Element; import org.projectforge.access.AccessType; import org.projectforge.access.GroupTaskAccessDO; import org.projectforge.access.OperationType; import org.projectforge.core.IdObject; import org.projectforge.core.UserException; import org.projectforge.fibu.ProjektDO; /** * Represents a single task as part of the TaskTree. The data of a task node is stored in the database. * @author Kai Reinhard (k.reinhard@micromata.de) */ public class TaskNode implements IdObject<Integer>, Serializable { private static final long serialVersionUID = -3759574521842841341L; /** For log messages. */ private static final Logger log = Logger.getLogger(TaskNode.class); /** Reference to the parent task node with the parentTaskID. */ TaskNode parent = null; ProjektDO projekt; /** Total duration of all time sheets of this task (excluding the child tasks) in seconds. */ long totalDuration = 0; /** * Sum of all ordered person days excluding descendant nodes. Ordered person days are defined by the sum of all assigned order position's * person days. Used and set by task tree. */ BigDecimal orderedPersonDays; /** * References to all child nodes in an ArrayList from element typ TaskNode. */ List<TaskNode> childs = null; /** The data of this TaskNode. */ TaskDO task = null; boolean bookableForTimesheets; /** * For every group with access to this node the permissions will be stored here. */ private final List<GroupTaskAccessDO> groupTaskAccessList = new ArrayList<GroupTaskAccessDO>(); public TaskNode() { } /** * @return True, if the parent task id of the underlying task is null, false otherwise. */ public boolean isRootNode() { return this.task.getParentTaskId() == null; } public void setTask(final TaskDO task) { this.task = task; } public TaskDO getTask() { return this.task; } /** The id of this task given by the database. */ public Integer getId() { return task.getId(); } /** The id of this task given by the database. */ public Integer getTaskId() { return task.getId(); } public Integer getParentId() { if (parent == null) { return null; } return parent.getId(); } /** Returns the parent task. */ public TaskNode getParent() { return this.parent; } public void internalSetParent(final TaskNode parent) { this.parent = parent; if (parent != null) { parent.addChild(this); } } /** * @return The reference of the assigned task, or if not given or blank of the parent task node. If the parent task node has no reference * the grand parent task reference will be assumed and so on. */ public String getReference() { if (StringUtils.isNotBlank(task.getReference()) == true) { return task.getReference(); } else if (parent != null) { return parent.getReference(); } else { return ""; } } /** * Gets the project, which is assigned to the task or if not found to the parent task or grand parent task etc. * @param taskId * @return null, if now project is assigned to this task or ancestor tasks. * @see ProjektDO#getTask() */ public ProjektDO getProjekt() { return getProjekt(true); } /** * Gets the project, which is assigned to the task or if not found to the parent task or grand parent task etc. * @param taskId * @param recursive If true then search the ancestor nodes for a given project. * @return null, if now project is assigned to this task or ancestor tasks. * @see ProjektDO#getTask() */ public ProjektDO getProjekt(final boolean recursive) { if (projekt != null) { return projekt; } else if (recursive == true && parent != null) { return parent.getProjekt(); } else { return null; } } public boolean isDeleted() { return task.isDeleted(); } /** * @return True if this node is closed/deleted or any ancestor node is closed/deleted. */ public boolean isFinished() { if (task.isDeleted() || task.getStatus() == TaskStatus.C) { return true; } if (parent != null) { return parent.isFinished(); } return false; } /** * @return the bookableForTimesheets */ public boolean isBookableForTimesheets() { return bookableForTimesheets; } public List<Integer> getDescendantIds() { final List<Integer> descendants = new ArrayList<Integer>(); getDescendantIds(descendants); return descendants; } private void getDescendantIds(final List<Integer> descendants) { if (this.childs != null) { for (final TaskNode node : this.childs) { if (descendants.contains(node.getId()) == false) { // Paranoia setting for cyclic references. descendants.add(node.getId()); node.getDescendantIds(descendants); } } } } public List<Integer> getAncestorIds() { final List<Integer> ancestors = new ArrayList<Integer>(); getAncestorIds(ancestors); return ancestors; } private void getAncestorIds(final List<Integer> ancestors) { if (this.parent != null) { if (ancestors.contains(this.parent.getId()) == false) { // Paranoia setting for cyclic references. ancestors.add(this.parent.getId()); this.parent.getAncestorIds(ancestors); } } } /** * Returns all childs of this task in an ArrayList with elements from type TaskNode. */ public List<TaskNode> getChilds() { return this.childs; } /** Has this task any childs? */ public boolean hasChilds() { return this.childs != null && this.childs.size() > 0 ? true : false; } /** Checks if the given node is a child / descendant of this node. */ public boolean isParentOf(final TaskNode node) { if (this.childs == null) { return false; } for (final TaskNode child : this.childs) { if (child.equals(node) == true) { return true; } else if (child.isParentOf(node) == true) { return true; } } return false; } /** * Returns the path to the root node in an ArrayList. The list contains also the current task. */ public List<TaskNode> getPathToRoot() { return getPathToAncestor(null); } /** * Returns the path to the parent node in an ArrayList. */ public List<TaskNode> getPathToAncestor(final Integer ancestorTaskId) { if (this.parent == null || this.task.getId().equals(ancestorTaskId) == true) { return new ArrayList<TaskNode>(); } final List<TaskNode> path = this.parent.getPathToAncestor(ancestorTaskId); path.add(this); return path; } /** * Sets / changes the parent of this node. This method does not modify the parent task! So it should be called only by TaskTree. */ void setParent(final TaskNode parent) { if (parent != null) { if (parent.getId().equals(getId()) == true || this.isParentOf(parent)) { log.error("Oups, cyclic reference detection: taskId = " + getId() + ", parentTaskId = " + parent.getId()); throw new UserException(TaskDao.I18N_KEY_ERROR_CYCLIC_REFERENCE); } this.parent = parent; this.task.setParentTask(parent.getTask()); } } /** * Adds a new task as a child of this node. It does not check wether this task already exist as child or not! This method does not modify * the child task! */ void addChild(final TaskNode child) { if (child != null) { if (child.getId().equals(getId()) == true || child.isParentOf(this)) { log.error("Oups, cyclic reference detection: taskId = " + getId() + ", parentTaskId = " + parent.getId()); return; } if (this.childs == null) { this.childs = new ArrayList<TaskNode>(); } this.childs.add(child); } } /** * Removes a child task of this node. This method does not modify the child task! */ void removeChild(final TaskNode child) { if (child == null) { log.error("Oups, child is null, can't remove it from parent."); } else if (this.childs == null) { log.error("Oups, this node has no childs to remove."); } else if (this.childs.contains(child) == false) { log.error("Oups, this node doesn't contain given child."); } else { log.debug("Removing child " + child.getTaskId() + " from parent " + this.getTaskId()); this.childs.remove(child); } } /** * Checks the desired permission for the given group to this task. If no GroupTaskAccess is defined for this task for the given group, * hasPermission will be called of the parent task. * * @param groupId The id of the group to check. * @param accessType TASK_ACCESS, ... * @param opType Select, insert, update or delete. * @see AccessType * @see OperationType */ public boolean hasPermission(final Integer groupId, final AccessType accessType, final OperationType opType) { final GroupTaskAccessDO groupAccess = getGroupTaskAccess(groupId); if (groupAccess == null) { if (parent != null) { return parent.isPermissionRecursive(groupId) && parent.hasPermission(groupId, accessType, opType); } // This is the root node. return false; } return groupAccess.hasPermission(accessType, opType); } public boolean isPermissionRecursive(final Integer groupId) { final GroupTaskAccessDO groupAccess = getGroupTaskAccess(groupId); return groupAccess == null || groupAccess.isRecursive() == true; } /** * Gets the GroupTaskAccessDO for the given group. * @param groupId * @return The GroupTaskAccessDO or null if not exists. */ GroupTaskAccessDO getGroupTaskAccess(final Integer groupId) { Validate.notNull(groupId); for (final GroupTaskAccessDO access : groupTaskAccessList) { if (groupId.equals(access.getGroupId()) == true) { return access; } } return null; } /** * Sets the task group access to this task node for the given group. Removes any previous stored GroupTaskAccessDO for the same group if * exists. Multiple GroupTaskAccessDO entries for one group will be avoided. * @param GroupTaskAccessDO */ void setGroupTaskAccess(final GroupTaskAccessDO groupTaskAccess) { Validate.isTrue(ObjectUtils.equals(this.getTaskId(), groupTaskAccess.getTaskId()) == true); // TODO: Should be called after update and insert into database. if (log.isInfoEnabled() == true) { log.debug("Set explicit access, taskId = " + getTaskId() + ", groupId = " + groupTaskAccess.getGroupId()); } synchronized (groupTaskAccessList) { removeGroupTaskAccess(groupTaskAccess.getGroupId()); groupTaskAccessList.add(groupTaskAccess); } } /** * Removes the GroupTaskAccessDO for the given group if exists. * @param groupId * @return true if an entry was found and removed, otherwise false. */ boolean removeGroupTaskAccess(final Integer groupId) { // TODO: Should be called after deleting from database. Validate.notNull(groupId); boolean result = false; synchronized (groupTaskAccessList) { final Iterator<GroupTaskAccessDO> it = groupTaskAccessList.iterator(); while (it.hasNext() == true) { final GroupTaskAccessDO access = it.next(); if (groupId.equals(access.getGroupId()) == true) { it.remove(); result = true; } } } return result; } /** * Gets the total duration of all time sheets in seconds. * @param recursive If true, then the durations of all time sheets of the sub tasks will be added. * @return */ public long getDuration(final TaskTree taskTree, final boolean recursive) { if (totalDuration < 0) { taskTree.readTotalDuration(this.getId()); } if (recursive == false || childs == null) { return totalDuration; } long duration = totalDuration; for (final TaskNode child : childs) { duration += child.getDuration(taskTree, true); } return duration; } @Override public boolean equals(final Object o) { if (o instanceof TaskNode) { final TaskNode other = (TaskNode) o; return ObjectUtils.equals(this.getParentId(), other.getParentId()) == true && ObjectUtils.equals(this.getTask().getTitle(), other.getTask().getTitle()) == true; } return false; } @Override public int hashCode() { final HashCodeBuilder hcb = new HashCodeBuilder(); hcb.append(this.getParentId()).append(this.getTask().getTitle()); return hcb.toHashCode(); } @Override public String toString() { final ToStringBuilder sb = new ToStringBuilder(this); sb.append("id", getId()); Object parentId = null; if (this.parent != null) { parentId = this.parent.getId(); } log.debug("id: " + this.getId() + ", parentId: " + parentId); sb.append("parent", parentId); sb.append("title", task.getTitle()); sb.append("childs", this.childs); return sb.toString(); } Element addXMLElement(final Element parent) { final Element el = parent.addElement("task").addAttribute("id", String.valueOf(this.getId())) .addAttribute("name", this.task.getTitle()); if (this.childs != null) { for (final TaskNode node : this.childs) { node.addXMLElement(el); } } return el; } }