/*******************************************************************************
* Copyright (c) 2004, 2010 BREDEX GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BREDEX GmbH - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.jubula.client.core.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.EntityManager;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.MapKeyColumn;
import javax.persistence.OneToMany;
import javax.persistence.OrderColumn;
import javax.persistence.Query;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.eclipse.jubula.client.core.businessprocess.problems.IProblem;
import org.eclipse.jubula.client.core.businessprocess.progress.ElementLoadedProgressListener;
import org.eclipse.jubula.client.core.businessprocess.progress.InsertProgressListener;
import org.eclipse.jubula.client.core.businessprocess.progress.RemoveProgressListener;
import org.eclipse.jubula.client.core.persistence.GeneralStorage;
import org.eclipse.jubula.client.core.persistence.PersistenceUtil;
import org.eclipse.jubula.client.core.utils.DependencyCheckerOp;
import org.eclipse.jubula.client.core.utils.NativeSQLUtils;
import org.eclipse.jubula.client.core.utils.TreeTraverser;
import org.eclipse.jubula.tools.internal.constants.StringConstants;
import org.eclipse.jubula.tools.internal.utils.EnvironmentUtils;
import org.eclipse.persistence.annotations.BatchFetch;
import org.eclipse.persistence.annotations.BatchFetchType;
import org.eclipse.persistence.annotations.Index;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for all kinds of nodes in test tree
*
* @author BREDEX GmbH
* @created 17.08.2004
*/
@Entity
@Table(name = "NODE")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(discriminatorType = DiscriminatorType.CHAR,
name = "CLASS_ID")
@DiscriminatorValue(value = "N")
@EntityListeners(value = {
ElementLoadedProgressListener.class,
InsertProgressListener.class, RemoveProgressListener.class })
abstract class NodePO implements INodePO {
/** the logger */
private static final Logger LOG = LoggerFactory
.getLogger(NodePO.class);
/** Persistence (JPA / EclipseLink) OID */
private transient Long m_id = null;
/** Globally Unique Identifier for recognizing nodes across databases */
private transient String m_guid = null;
/** Persistence (JPA / EclipseLink) version id */
private transient Integer m_version = null;
/** Flag if the parent at the children is set.
* @see getNodeList()
*/
private transient boolean m_isParentNodeSet = false;
/**
* generated tag
*/
private boolean m_isGenerated;
/**
* whether this element has been marked as "active" or "inactive"
*/
private boolean m_isActive = true;
/**
* The current toolkit level of this node.
* Not to persist!
*/
private transient String m_toolkitLevel = StringConstants.EMPTY;
/**
* name of the real node, e.g. CapPO name or Testcase name
*/
private String m_name;
/**
* the task Id of the node
*/
private String m_taskId;
/**
* The changed info is a map with a time stamp as key and a comment as value.
*/
private Map<Long, String> m_trackedChangesMap = new HashMap<Long, String>();
/**
* describes, if the node is derived from another node
*/
private INodePO m_parentNode = null;
/** The ID of the parent project */
private Long m_parentProjectId = null;
/**
* list of all child nodes, if existent
*/
private List<INodePO> m_nodeList = new ArrayList<INodePO>();
/**
* contains the comment for a node
*/
private String m_comment;
/** The timestamp */
private long m_timestamp = 0;
/** set of problems */
private Set<IProblem> m_problems = new HashSet<IProblem>(5);
/** string for description */
private String m_description;
/** */
private boolean m_isJUnitSuite;
/**
* constructor for a node with a pre-existing GUID
* @param name of the node
* @param guid of the node
* @param isGenerated indicates whether this node has been generated
*/
protected NodePO(String name, String guid, boolean isGenerated) {
setName(name);
setGuid(guid);
setGenerated(isGenerated);
}
/**
* constructor
* @param name of the node
* @param isGenerated indicates whether this node has been generated
*/
protected NodePO(String name, boolean isGenerated) {
this(name, PersistenceUtil.generateUUID(), isGenerated);
}
/**
* only for Persistence (JPA / EclipseLink)
*/
NodePO() {
// only for Persistence (JPA / EclipseLink)
}
/**
* @param nodeList The nodeList to set.
*/
void setHbmNodeList(List<INodePO> nodeList) {
m_nodeList = nodeList;
m_isParentNodeSet = false;
}
/**
*
* {@inheritDoc}
* @return The name of this node
*/
@Transient
public String getName() {
return getHbmName();
}
/**
* gets the value of the m_name property
*
* @return the name of the node
*/
@Basic
@Column(name = "NAME", length = MAX_STRING_LENGTH)
private String getHbmName() {
return m_name;
}
/**
* For Persistence (JPA / EclipseLink) only
* Sets the value of the m_name property.
*
* @param name
* the new value of the m_name property
*/
private void setHbmName(String name) {
m_name = name;
}
/**
* Sets the value of the m_name property.
* @param name the name of this node
*/
public void setName(String name) {
setHbmName(name);
}
/**
* @return the current value of the m_parentNode property or null
*/
@Transient
public INodePO getParentNode() {
return m_parentNode;
}
/**
* @param parent parent to set
*/
public void setParentNode(INodePO parent) {
if (LOG.isErrorEnabled() && parent == null) {
try {
throw new IllegalArgumentException(
"The parent of the INodePO (UUID " + getGuid() //$NON-NLS-1$
+ ") is not intended to be set to null."); //$NON-NLS-1$
} catch (IllegalArgumentException e) {
LOG.info(ExceptionUtils.getFullStackTrace(e), e);
}
}
m_parentNode = parent;
}
/**
*
* Access method for the m_nodeList property.
* only to use for Persistence (JPA / EclipseLink)
*
* The reason for the Many-To-Many is that Eclipselink cannot deal with moving around
* nodes of a tree which have grandchildren...
*
* @return the current value of the m_nodeList property
*/
@OneToMany(fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
targetEntity = NodePO.class)
@JoinColumn(name = "PARENT")
@OrderColumn(name = "IDX")
@BatchFetch(value = BatchFetchType.JOIN)
List<INodePO> getHbmNodeList() {
return m_nodeList;
}
/**
* Frees the node children of the node, lets them handle their children and deletes them
* @param sess the session
*/
private void removeNodeChildren(EntityManager sess) {
if (m_nodeList.isEmpty()) {
return;
}
for (INodePO child : m_nodeList) {
child.goingToBeDeleted(sess);
}
Query q = sess.createNativeQuery("delete from NODE where ID in " + NativeSQLUtils.getIdList(m_nodeList)); //$NON-NLS-1$
q.executeUpdate();
}
/**
* @return The List of children nodes
*/
@Transient
List<INodePO> getNodeList() {
if (!m_isParentNodeSet) {
List<INodePO> nodeList = getHbmNodeList();
for (Object o : nodeList) {
INodePO node = (INodePO)o;
node.setParentNode(this);
}
m_isParentNodeSet = true;
}
return getHbmNodeList();
}
/**
* @return the unmodifiable node list.
*/
@Transient
public List<INodePO> getUnmodifiableNodeList() {
return Collections.unmodifiableList(getNodeList());
}
/**
*
* @return Returns the m_comment.
*/
@Basic
@Column(name = "COMM_TXT", length = MAX_STRING_LENGTH)
private String getHbmComment() {
return m_comment;
}
/**
* @return Returns the m_comment.
*/
@Transient
public String getComment() {
return getHbmComment();
}
/**
* For Persistence (JPA / EclipseLink) only
* @param comment The m_comment to set.
*/
private void setHbmComment(String comment) {
m_comment = comment;
}
/**
* @param comment The m_comment to set.
*/
public void setComment(String comment) {
setHbmComment(comment);
}
/**
* adds a childnode to an existent node
* creation of reference to the parent node
* @param childNode
* reference to the childnode
*/
public void addNode(INodePO childNode) {
addNode(-1, childNode);
}
/**
* adds a childnode to an existent node
* creation of reference to the parent node
* @param position the position to add the childnode.
* @param childNode
* reference to the childnode
*/
public void addNode(int position, INodePO childNode) {
if (position < 0 || position > getNodeList().size()) {
getNodeList().add(childNode);
} else {
getNodeList().add(position, childNode);
}
childNode.setParentNode(this);
setParentProjectIdForChildNode(childNode);
}
/**
* Sets the child node's parentProjectId equal to this node's parentProjectId.
* This is the default implementation. Subclasses may override.
*
* @param childNode The node that will have its parentProjectId set.
*/
protected void setParentProjectIdForChildNode(INodePO childNode) {
childNode.setParentProjectId(getParentProjectId());
}
/**
* deletes a node and resolves the
* reference to the parent node
* sign off as child node of the parent node
* @param childNode reference to the childnode
*/
public void removeNode(INodePO childNode) {
((NodePO)childNode).removeMe(this);
}
/**
* @param parent removes the node from childrenList or eventhandlerMap
*/
protected void removeMe(INodePO parent) {
((NodePO)parent).getNodeList().remove(this);
}
/**
* Removes all child nodes and sets the parent of the child nodes
* to <code>null</code>
*/
public void removeAllNodes() {
Iterator<INodePO> iter = getNodeList().iterator();
while (iter.hasNext()) {
INodePO childNode = iter.next();
childNode.setParentNode(null);
iter.remove();
}
}
/**
* Returns the index of the given node in the node list.
* @param node the node whose index is want.
* @return the index of the given node.
*/
public int indexOf(INodePO node) {
return getNodeList().indexOf(node);
}
/**
* Returns the valid status of the node.<br>
* Normally all Nodes are valid. only CapPOs with an InvalidComponent
* should return false.
* @return true if the Node is valid, false otherwise.
*/
@Transient
public boolean isValid() {
return true;
}
/** {@inheritDoc} */
public int hashCode() { // NOPMD by al on 3/19/07 1:35 PM
return getGuid().hashCode();
}
/**
*
* {@inheritDoc}
*/
@Transient
public Iterator<INodePO> getNodeListIterator() {
return Collections.unmodifiableList(getNodeList()).iterator();
}
/**
* @return size of nodeList
*/
@Transient
public int getNodeListSize() {
return getNodeList().size();
}
/**
* {@inheritDoc}
*/
public String toString() {
return super.toString() + StringConstants.SPACE
+ StringConstants.LEFT_PARENTHESIS + getName()
+ StringConstants.RIGHT_PARENTHESIS;
}
/**
* @return Returns the id.
*/
@Id
@GeneratedValue
public Long getId() {
return m_id;
}
/**
* @param id The id to set.
*/
void setId(Long id) {
m_id = id;
}
/**
*
* @return Long
*/
@Version
@Column(name = "VERSION")
public Integer getVersion() {
return m_version;
}
/**
* @param version The version to set.
*/
@SuppressWarnings("unused")
private void setVersion(Integer version) {
m_version = version;
}
/**
*
* @return the GUID.
*/
@Basic
@Column(name = "GUID")
@Index(name = "PI_NODE_GUID")
public String getGuid() {
return m_guid;
}
/**
* @param guid The GUID to set.
*/
private void setGuid(String guid) {
m_guid = guid;
}
/**
* Checks for circular dependences with a potential parent.
* @param parent the parent to check
* @return true if there is a circular dependence, false otherwise.
*/
public boolean hasCircularDependences(INodePO parent) {
DependencyCheckerOp op = new DependencyCheckerOp(parent);
TreeTraverser traverser = new TreeTraverser(this, op);
traverser.traverse(true);
return op.hasDependency();
}
/**
* Checks the equality of the given Object with this Object.
* {@inheritDoc}
* @param obj the object to check
* @return if there is a database ID it returns true if the ID is equal.
* If there is no ID it will be compared to identity.
*/
public boolean equals(Object obj) { // NOPMD by al on 3/19/07 1:35 PM
if (this == obj) {
return true;
}
if (!(obj instanceof NodePO || obj instanceof INodePO)) {
return false;
}
INodePO o = (INodePO)obj;
return getGuid().equals(o.getGuid());
}
/**
*
* {@inheritDoc}
*/
@Transient
public Long getParentProjectId() {
return getHbmParentProjectId();
}
/**
*
* {@inheritDoc}
*/
public void setParentProjectId(Long projectId) {
setHbmParentProjectId(projectId);
for (INodePO node : getHbmNodeList()) {
node.setParentProjectId(projectId);
}
}
/**
*
* {@inheritDoc}
*/
@Basic
@Column(name = "PARENT_PROJ")
@Index(name = "PI_NODE_PARENT_PROJ")
Long getHbmParentProjectId() {
return m_parentProjectId;
}
/**
*
* {@inheritDoc}
*/
void setHbmParentProjectId(Long projectId) {
m_parentProjectId = projectId;
}
/**
* @return the current toolkit level of this node.
*/
@Transient
public String getToolkitLevel() {
return m_toolkitLevel;
}
/**
* Sets the current toolkit level of this node.
* @param toolkitLevel the toolkit level.
*/
public void setToolkitLevel(String toolkitLevel) {
m_toolkitLevel = toolkitLevel;
}
/**
*
* {@inheritDoc}
*/
@Basic
public long getTimestamp() {
return m_timestamp;
}
/**
* {@inheritDoc}
*/
public void setTimestamp(long timestamp) {
m_timestamp = timestamp;
}
/**
*
* @return the isGenerated Attribute for all nodes
*/
@Basic(optional = false)
@Column(name = "IS_GENERATED")
public boolean isGenerated() {
return m_isGenerated;
}
/**
* @param isGenerated the isGenerated to set
*/
public void setGenerated(boolean isGenerated) {
m_isGenerated = isGenerated;
}
/**
* @param isActive the isActive to set
*/
public void setActive(boolean isActive) {
m_isActive = isActive;
}
/**
*
* @return the isActive Attribute for all nodes
*/
@Basic(optional = false)
@Column(name = "IS_ACTIVE")
public boolean isActive() {
return m_isActive;
}
/** {@inheritDoc} */
public boolean addProblem(IProblem problem) {
if (isActive()) {
return m_problems.add(problem);
}
return false;
}
/** {@inheritDoc} */
public boolean removeProblem(IProblem problem) {
return m_problems.remove(problem);
}
/** {@inheritDoc} */
public Set<IProblem> getProblems() {
return Collections.unmodifiableSet(m_problems);
}
/**
* gets the value of the taskId property
*
* @return the taskId of the node
*/
@Basic
@Column(name = "TASK_ID", length = MAX_STRING_LENGTH)
public String getTaskId() {
return m_taskId;
}
/**
* For Persistence (JPA / EclipseLink) only
* Sets the value of the taskId property. If the length of
* the trimmed new taskId string is zero, the taskId property
* is set to null.
*
* @param taskId
* the new value of the taskId property
*/
public void setTaskId(String taskId) {
String newTaskId = taskId;
if (newTaskId != null) {
newTaskId = newTaskId.trim();
if (newTaskId.length() == 0) {
newTaskId = null;
}
}
m_taskId = newTaskId;
}
/**
* Only for Persistence (JPA / EclipseLink).
* @param trackedChangesMap The tracked changes as a map of time stamp as key and comment as value.
*/
public void setTrackedChangesMap(Map<Long, String> trackedChangesMap) {
this.m_trackedChangesMap = trackedChangesMap;
}
/**
* Only for Persistence (JPA / EclipseLink).
* @return The map of change information.
*/
@ElementCollection
@CollectionTable(name = "NODE_TRACK",
joinColumns = @JoinColumn(name = "NODE_ID", nullable = false))
@MapKeyColumn(name = "TIMESTAMP", nullable = false)
@Column(name = "TRACK_COMMENT")
private Map<Long, String> getTrackedChangesMap() {
return m_trackedChangesMap;
}
/**
* Removes the track children of the node
* @param sess the session
*/
private void removeTrackChildren(EntityManager sess) {
Query q = sess.createNativeQuery("delete from NODE_TRACK where NODE_ID = ?1"); //$NON-NLS-1$
q.setParameter(1, getId());
q.executeUpdate();
}
/**
* {@inheritDoc}
*/
public void addTrackedChange(String optionalComment, boolean isCleaning) {
final IProjectPO project = GeneralStorage.getInstance().getProject();
if (project == null || !project.getIsTrackingActivated()) {
return; // no project is opened or tracking not activated
}
final long timestampInMS = new Date().getTime();
if (isCleaning) { // remove tracked changes
int span = project.getProjectProperties().getTrackChangesSpan();
switch (project.getProjectProperties().getTrackChangesUnit()) {
case CHANGES:
removeTrackedChangesByCount(span - 1); // -1 for the new one
break;
case DAYS:
removeTrackedChangesByTimeInDays(timestampInMS, span);
break;
default:
return;
}
}
final String name = project.getProjectProperties()
.getTrackChangesSignature();
String value = StringConstants.EMPTY;
if (!StringUtils.isEmpty(name)) {
value = EnvironmentUtils.getProcessOrSystemProperty(name);
}
if (value != null) {
StringBuffer comment = new StringBuffer(value);
if (optionalComment.length() > 0) {
if (comment.length() > 0) {
comment.append(StringConstants.COLON);
comment.append(StringConstants.SPACE);
}
comment.append(optionalComment);
}
m_trackedChangesMap.put(timestampInMS, comment.toString());
}
}
/**
* Remove tracked changes of this node beginning with the oldest, if the maximum number has reached.
* @param maxTrackedChangesPerNode The maximum allowed number of tracked changes per node.
*/
private void removeTrackedChangesByCount(
final int maxTrackedChangesPerNode) {
if (maxTrackedChangesPerNode >= 0) {
SortedMap<Long, String> copy = getTrackedChanges();
for (int removeCount = m_trackedChangesMap.size()
- maxTrackedChangesPerNode;
removeCount > 0; removeCount--) {
Long key = copy.lastKey();
copy.remove(key);
m_trackedChangesMap.remove(key);
}
}
}
/**
* Remove tracked changes of this node beginning with the oldest, if they are created before
* the given time stamp.
* @param currentTimesInMS The current time stamp in milliseconds
* @param days The maximum allowed number in days of tracked changes per node.
*/
private void removeTrackedChangesByTimeInDays(
final long currentTimesInMS, final int days) {
if (days >= 0) {
final long durationOfDayInMS = 1000L * 60 * 60 * 24;
final long beginningInMS =
currentTimesInMS - days * durationOfDayInMS;
SortedMap<Long, String> copy = getTrackedChanges();
while (copy.size() > 0 && copy.lastKey() < beginningInMS) {
Long key = copy.lastKey();
copy.remove(key);
m_trackedChangesMap.remove(key);
}
}
}
/**
* {@inheritDoc}
*/
public SortedMap<Long, String> getTrackedChanges() {
SortedMap<Long, String> sortedMap = new TreeMap<Long, String>(
Collections.reverseOrder());
sortedMap.putAll(m_trackedChangesMap);
return sortedMap;
}
/**
* {@inheritDoc}
*/
public void deleteTrackedChanges() {
m_trackedChangesMap.clear();
}
/**
*
* @return Returns the m_description.
*/
@Column(name = "DESCRIPTION_TXT")
@Lob
public String getDescription() {
return m_description;
}
/**
* @param description The m_description to set.
*/
public void setDescription(String description) {
m_description = description;
}
/** {@inheritDoc} */
@Transient
public Iterator<INodePO> getAllNodeIter() {
return getNodeListIterator();
}
/** {@inheritDoc} */
@Transient
public INodePO getSpecAncestor() {
INodePO par = this;
while (par != null && !(par instanceof ISpecTestCasePO
|| par instanceof ITestSuitePO || par instanceof ITestJobPO)) {
par = par.getParentNode();
}
return par;
}
/** {@inheritDoc} */
public void goingToBeDeleted(EntityManager sess) {
removeNodeChildren(sess);
removeTrackChildren(sess);
}
/**
* @return return wether the Node acts as a JUnit testsuite
*/
@Basic
@Column(name = "JUNITTESTSUITE")
public boolean isJUnitTestSuite() {
return m_isJUnitSuite;
}
/**
* @param isJUnitSuite true = node is viewed as a testsuite
*/
public void setJUnitTestSuite(boolean isJUnitSuite) {
m_isJUnitSuite = isJUnitSuite;
}
}