///////////////////////////////////////////////////////////////////////////// // // 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.plugins.skillmatrix; import java.io.Serializable; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.apache.log4j.Logger; import org.projectforge.common.AbstractCache; import org.projectforge.registry.Registry; import org.projectforge.user.ProjectForgeGroup; /** * Holds the complete skill list as a tree. It will be initialized by the values read from the database. Any changes will be written to this * tree and to the database. * @author Billy Duong (b.duong@micromata.de) */ public class SkillTree extends AbstractCache implements Serializable { private static final long serialVersionUID = 8331566269626909676L; private static final Logger log = Logger.getLogger(SkillTree.class); private static final List<SkillNode> EMPTY_LIST = new ArrayList<SkillNode>(); private SkillDao skillDao; /** For faster searching of entries. */ private Map<Integer, SkillNode> skillMap; /** The root node of all skills. The only node with parent null. */ private SkillNode root = null; /** Time of last modification in milliseconds from 1970-01-01. */ private long timeOfLastModification = 0; public SkillNode getRootSkillNode() { checkRefresh(); return this.root; } /** Adds the given node as child of the given parent. */ private synchronized SkillNode addSkillNode(final SkillNode node, final SkillNode parent) { checkRefresh(); if (parent != null) { node.setParent(parent); parent.addChild(node); } updateTimeOfLastModification(); return node; } /** * Adds a new node with the given data. The given skill holds all data and the information (id) of the parent node of the node to add. * Will be called by SkillDao after inserting a new skill. */ SkillNode addSkillNode(final SkillDO skill) { checkRefresh(); final SkillNode node = new SkillNode(); node.setSkill(skill); final SkillNode parent = getSkillNodeById(skill.getParentId()); if (parent != null) { node.setParent(parent); } else if (node.getId().equals(root.getId()) == false) { // This node is not the root node: node.setParent(root); } skillMap.put(node.getId(), node); return addSkillNode(node, parent); } /** * @param skillId * @param ancestorSkillId * @return * @see SkillNode#getPathToAncestor(Integer) */ public List<SkillNode> getPath(final Integer skillId, final Integer ancestorSkillId) { checkRefresh(); if (skillId == null) { return EMPTY_LIST; } final SkillNode skillNode = getSkillNodeById(skillId); if (skillNode == null) { return EMPTY_LIST; } return skillNode.getPathToAncestor(ancestorSkillId); } /** * Returns the path to the root node in an ArrayList. * @see #getPath(Integer, Integer) */ public List<SkillNode> getPathToRoot(final Integer skillId) { return getPath(skillId, null); } /** All skill nodes are stored in an HashMap for faster searching. */ public SkillNode getSkillNodeById(final Integer id) { if (id == null) { return null; } checkRefresh(); return skillMap.get(id); } public SkillDO getSkill(final String title) { if (StringUtils.isEmpty(title)) { return null; } checkRefresh(); for (final SkillNode skill : skillMap.values()) { if (title.equals(skill.getSkill().getTitle())) { return skill.getSkill(); } } return null; } public SkillDO getSkillById(final Integer id) { checkRefresh(); final SkillNode node = getSkillNodeById(id); if (node != null) { return node.getSkill(); } return null; } /** * Has the current logged in user select access to the given skill? * @param node * @return */ public boolean hasSelectAccess(final SkillNode node) { return skillDao.hasLoggedInUserSelectAccess(node.getSkill(), false); } /** * After changing a skill this method will be called by SkillDao for updating the skill and the skill tree. * @param skill Updating the existing skill in the SkillTree. If not exist, a new skill will be added. */ SkillNode addOrUpdateSkillNode(final SkillDO skill) { checkRefresh(); Validate.notNull(skill); Validate.notNull(skill.getId()); final SkillNode node = getSkillNodeById(skill.getId()); if (node == null) { return addSkillNode(skill); } node.setSkill(skill); if (skill.getParentId() != null && skill.getParentId().equals(node.getParent().getId()) == false) { if (log.isDebugEnabled() == true) { log.debug("Skill hierarchy was changed for skill: " + skill); } final SkillNode oldParent = node.getParent(); Validate.notNull(oldParent); oldParent.removeChild(node); final SkillNode newParent = getSkillNodeById(skill.getParentId()); node.setParent(newParent); newParent.addChild(node); } return node; } public SkillTree() { } /** * @see #isRootNode(SkillDO) */ public boolean isRootNode(final SkillNode node) { Validate.notNull(node); return isRootNode(node.getSkill()); } /** * @param node * @return true, if the given skill has the same id as the Skill tree's root node, otherwise false; */ public boolean isRootNode(final SkillDO skill) { Validate.notNull(skill); checkRefresh(); if (root == null && skill.getParentId() == null) { // First skill, so it should be the root node. return true; } if (skill.getId() == null) { // Node has no id, so it can't be the root node. return false; } return root.getId().equals(skill.getId()); } /** * Should only called by test suite! */ public void clear() { this.root = null; this.setExpired(); } /** * All skills from database will be read and cached into this SkillTree. Also all explicit group skill access' will be read from database * and will be cached in this tree (implicit access' will be created too).<br/> * The generation of the skill tree will be done manually, not by Hibernate because the skill hierarchy is very sensible. Manipulations of * the skill tree should be done carefully for single skill nodes. * * @see org.projectforge.common.AbstractCache#refresh() */ @Override protected void refresh() { log.info("Initializing skill tree ..."); SkillNode newRoot = null; skillMap = new HashMap<Integer, SkillNode>(); final List<SkillDO> skillList = skillDao.internalLoadAll(); SkillNode node; log.debug("Loading list of skills ..."); for (final SkillDO skill : skillList) { node = new SkillNode(); node.setSkill(skill); skillMap.put(node.getId(), node); if (node.isRootNode() == true) { if (newRoot != null) { log.error("Duplicate root node found: " + newRoot.getId() + " and " + node.getId()); node.setParent(newRoot); // Set the second root skill as child skill of first read root skill. } else { if (log.isDebugEnabled() == true) { log.debug("Root note found: " + node); } newRoot = node; } } } if (newRoot == null) { log.fatal("OUPS, no skill found (ProjectForge database not initialized?) OK, initialize it ..."); newRoot = createRootNode(); skillMap.put(newRoot.getId(), newRoot); } this.root = newRoot; if (log.isDebugEnabled() == true) { log.debug("Creating tree for " + skillList.size() + " skills ..."); } for (final SkillDO skill : skillList) { SkillNode parentNode = null; node = skillMap.get(skill.getId()); final Integer parentId = skill.getParentId(); if (parentId != null) { parentNode = skillMap.get(parentId); } // log.debug("Processing node: " + node.getId() + ", parent: " + parentId); if (parentNode != null) { node.setParent(parentNode); parentNode.addChild(node); updateTimeOfLastModification(); } else { log.debug("Processing root node:" + node); } } if (log.isDebugEnabled() == true) { log.debug(this.root); } if (log.isDebugEnabled() == true) { log.debug(this.toString()); } log.info("Initializing skill tree done."); } /** * @return the timeOfLastModification */ public long getTimeOfLastModification() { return timeOfLastModification; } /** * @param timeOfLastModification the timeOfLastModification to set */ public void setTimeOfLastModification(final long timeOfLastModification) { this.timeOfLastModification = timeOfLastModification; } private void updateTimeOfLastModification() { this.timeOfLastModification = new Date().getTime(); } /** * @param skillDao the skillDao to set * @return this for chaining. */ SkillTree setSkillDao(final SkillDao skillDao) { this.skillDao = skillDao; return this; } /** * Creates a root node for the skill tree and saves it in the database. * @return a "dummy" skill node. */ private SkillNode createRootNode() { final SkillNode newRoot = new SkillNode(); final SkillDO rootSkill = new SkillDO(); rootSkill.setTitle("root"); rootSkill.setDescription("ProjectForge root skill"); rootSkill.setRateable(false); final String s = Registry.instance().getUserGroupCache().getGroup(ProjectForgeGroup.ADMIN_GROUP).getId().toString(); rootSkill.setFullAccessGroupIds(s).setReadOnlyAccessGroupIds(s).setTrainingAccessGroupIds(s); // TODO internalSave gives a no hibernate session bound to thread warning, this workaround should probably exchanged for a better solution // skillDao.internalSave(rootSkill); skillDao.getHibernateTemplate().save(rootSkill); newRoot.setSkill(rootSkill); return newRoot; } }