/*******************************************************************************
* Copyright 2006 - 2012 Vienna University of Technology,
* Department of Software Technology and Interactive Systems, IFS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This work originates from the Planets project, co-funded by the European Union under the Sixth Framework Programme.
******************************************************************************/
package eu.scape_project.planning.model.tree;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import org.hibernate.annotations.IndexColumn;
import eu.scape_project.planning.model.Alternative;
import eu.scape_project.planning.model.ChangeLog;
import eu.scape_project.planning.model.EvaluationStatus;
import eu.scape_project.planning.model.IChangesHandler;
import eu.scape_project.planning.model.ITouchable;
import eu.scape_project.planning.validation.ValidationError;
/**
* Base class for our composite hierarchy of nodes and leaves - TreeNode
* corresponds to the <code>Component</code> in the Composite Design Pattern.
*
* @author Christoph Becker
* @see Node
* @see Leaf
*/
@Entity
@Inheritance
@DiscriminatorColumn(name = "nodetype")
public abstract class TreeNode implements ITreeNode, Serializable, ITouchable, Cloneable {
private static final long serialVersionUID = 7696297425270759326L;
@Id
@GeneratedValue
protected int id;
@ManyToOne
@JoinColumn(name = "parent_fk", insertable = false, updatable = false)
protected TreeNode parent;
@Lob
protected String name;
@Lob
protected String description;
/**
* indicates whether the weight of this node may be changed by the automatic
* balancing of weights. If lock is true, the weight is not changed
* automatically.
*/
@Column(name = "locked")
private boolean lock;
/**
* FIXME: why should it be more comfortable?
*
* determines if this node is going to have one single value for all
* SampleRecords, or if each SampleObject has its own result value. This
* only applies for the class Leaf, but it's comfier to include it here than
* write complex statements in the view layer.
*/
private boolean single;
/**
* This should never have a value with more than two fractional (decimal)
* digits! Kevin suggests turning this into an integer (0 <= x <= 100(0))
*/
protected double weight = 1.0;
/**
* the children that are contained in this node.
*/
@Valid
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "parent_fk")
@IndexColumn(name = "indexcol", base = 1)
protected List<TreeNode> children = new ArrayList<TreeNode>();
@ManyToOne(cascade = CascadeType.ALL)
private ChangeLog changeLog = new ChangeLog();
/**
* emtpy default constructor
*/
public TreeNode() {
}
/**
* Instantiate a new TreeNode by its name and according weight.
*
* @param name
* of the node.
* @param weight
* of the node.
*/
public TreeNode(String name, double weight) {
this.name = name;
this.weight = weight;
}
public ChangeLog getChangeLog() {
return changeLog;
}
public void setChangeLog(ChangeLog changeLog) {
this.changeLog = changeLog;
}
/**
* implement in subclasses.
*/
public void convertChild(TreeNode n) {
}
// /**
// * transient needed because of hibernate
// */
// @Transient
// private List<Leaf> allLeaves = null;
//
// /**
// * transient needed because of hibernate
// */
// @Transient
// private int numberOfLeaves = -1;
// /**
// * transient needed because of hibernate
// */
// @Transient
// private EvaluationStatus evaluationStatus = null;
//
/**
* @return the status of evaluation of the leaves of this branch of the
* tree, which can be one of the values that are defined in
* {@link EvaluationStatus}
* @see EvaluationStatus
*/
public abstract EvaluationStatus getEvaluationStatus();
/**
* @return a List with all the leaves that are contained in this branch of
* the tree
*/
public List<Leaf> getAllLeaves() {
List<Leaf> list = new ArrayList<Leaf>();
for (TreeNode n : children) {
if (n instanceof Leaf) {
Leaf leaf = (Leaf) n;
list.add(leaf);
} else {
list.addAll(n.getAllLeaves());
}
}
return list;
}
/**
*
* @return a List with all nodes that are contained in this branch of the tree
*/
public List<TreeNode> getAllChildren() {
List<TreeNode> list = new ArrayList<TreeNode>();
for (TreeNode n : children) {
list.add(n);
if (! (n instanceof Leaf)) {
list.addAll(n.getAllChildren());
}
}
return list;
}
/**
* Initializes the weights for all leaves of this TreeNode, i.e.
* {@link #balanceNodes(List, double) balances the weights} equally. This is
* done {@link #initWeights() recursively}.
*/
public void initWeights() {
if (children.isEmpty()) {
return;
}
// Set weights..
this.balanceNodes(children, 1.0);
// ..recursively!
for (TreeNode n : children) {
n.initWeights();
}
}
public boolean isLock() {
return lock;
}
public void setLock(boolean lock) {
this.lock = lock;
}
/**
* @return the number of leaves that are contained in this branch of the
* tree
*/
public int getNumberOfLeaves() {
return getAllLeaves().size();
}
/**
* This returns the absolute influence this Node has on the overall weighted
* root value. It is worthwhile displaying this in the Analysis stage - and
* the weighting stage - to give users a feeling how much influence each
* node has.
*/
public double getTotalWeight() {
return (getParent() == null) ? getWeight() : getWeight() * getParent().getTotalWeight();
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
/**
* sets the weight of this node, constraining it to [0,1]. Outliers are
* ignored, i.e. to 0 or 1, respectively.
*
* @param weight
* the new weight to be set
*/
public void setWeight(double weight) {
if (weight < 0) {
this.weight = 0.0;
} else if (weight > 1.0) {
this.weight = 1.0;
} else {
this.weight = weight;
}
}
public double getWeight() {
return weight;
}
public TreeNode getParent() {
return parent;
}
public void setParent(TreeNode parent) {
this.parent = parent;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public List<TreeNode> getChildren() {
return children;
}
/**
* sets the children, and sets self as parent of each of the nodes in the
* provided list
*
* @param children
* @see TreeNode#setParent(TreeNode)
*/
public void setChildren(List<TreeNode> children) {
this.children = children;
for (TreeNode n : children) {
n.setParent(this);
}
}
/**
* @return if this is a {@link Leaf}, false otherwise.
*/
public abstract boolean isLeaf();
/**
* returns the sibling that comes after self, or <code>null</code>, if
* either I am the last one or I don't have a parent.
*
* @return the next sibling, or null
*/
public TreeNode getNextSibling() {
if (parent != null) {
return parent.getNextChild(this);
} else {
return null;
}
}
/**
* returns the TreeNode that comes next in the children's list after the one
* that is being passed as parameter
*
* @param n
* node for which the next sibling shall be returned
* @return the next sibling of the provided node, or null, if there is none.
* @throws {@link IllegalArgumentException} if a foreign child is provided
* as a parameter
*/
public TreeNode getNextChild(TreeNode n) {
int index = children.indexOf(n);
if (index == -1) {
throw new IllegalArgumentException("This node is not my child. And I don't even think of adopting it.");
}
if (index == children.size()) {
return null;
} else {
return children.get(index + 1);
}
}
/**
* inits the values of all children
*
* @param list
* the alternatives for which values shall be created
* @param records
* the number of sample objects
* @param initLinkage
* indicates whether the linkage between scales and values shall
* be created.
* @see Leaf#initValues(List, int, boolean)
*/
public void initValues(List<Alternative> list, int records, boolean initLinkage) {
for (TreeNode n : children) {
n.initValues(list, records, initLinkage);
}
}
public boolean isValueMapProperlyInitialized(List<Alternative> alternatives, int numberRecords) {
for (TreeNode n : children) {
if (n.isValueMapProperlyInitialized(alternatives, numberRecords) == false) {
return false;
}
}
return true;
}
public boolean isSingle() {
return single;
}
public void setSingle(boolean single) {
this.single = single;
}
/**
* balances the importance weightings of self and my siblings by calling
* self's parent.
*
* @see #balanceWeights(TreeNode)
*/
public void balanceWeights() {
if (parent != null) {
parent.balanceWeights(this);
}
}
/**
* balances the weights of the children according to the newly changed
* weight of one child, and sets the changed child to locked state. If the
* sum of locked weights plus the changed one is over 1.0, the weight of the
* freshly changed node is cut down to the remaining difference of locked
* weights to 1.0. The other unlocked weights are balanced evenly.
*
* @param changed
* the node where the weight was just changed.
*/
public void balanceWeights(TreeNode changed) {
// We need to things:
// 1. The list of nodes where the weights are not locked
List<TreeNode> unlocked = new ArrayList<TreeNode>();
// 2. the sum of weights of the nodes where lock is true
double lockedWeight = 0.0;
// We calculate these two things:
for (TreeNode n : children) {
if (n != changed) {
if (n.isLock()) {
lockedWeight += n.getWeight();
} else {
unlocked.add(n);
}
}
}
if (lockedWeight + changed.getWeight() >= 1.0) {
/*
* Either the new change results in sum >= 1 in which case the
* changed gets only as much as weighting as is left in the pot, or
* there is enough left.
*
* First case treated here. If the locked weights are already too
* high, we dont give it everything: Even the unlocked nodes need a
* minimum of 0.01, so that amount is kept aside.
*/
changed.setWeight(1.0 - lockedWeight - 0.01 * unlocked.size());
balanceNodes(unlocked, 0.01 * unlocked.size());
} else {
/*
* Or the sum is < 1.0, which means we have to spread the remaining
* (1.0 - sum) evenly across the nodes that are not locked. Because
* the weights we set have to be fully transparent to the user
* (because he has to make sure that the sum is 1.0 in the end) we
* must not set a weight to something with more than two fractional
* (decimal!) digits!
*/
double weightToGive = 1.0 - (lockedWeight + changed.getWeight());
balanceNodes(unlocked, weightToGive);
}
// this node has been changed
changed.touch();
changed.setLock(true);
}
/**
* Spreads the given weight equally among the given TreeNodes, never using
* more than 2 fractional (decimal) digits
*
* @author Kevin Stadler
*/
private void balanceNodes(List<TreeNode> elements, double weightToGive) {
int i = elements.size();
/*
* "i" is the number of unlocked children of this node which have not
* been assigned a new weight yet
*/
for (TreeNode n : elements) {
// Round it to two decimal digits
double weight = Math.round((100.0 * weightToGive) / i) / 100.0;
n.setWeight(weight);
weightToGive -= weight;
i--;
// this node has been changed
n.touch();
}
}
/**
* checks if this is a child. If it has a parent, it must be a child, so...
*
* @return <code>not parent is null :)</code>
*/
public boolean isChild() {
return parent != null;
}
/**
* checks whether this node and its children are specified completely. This
* is overridden in Node and Leaf!
*
* @param errors
* TODO
* @see Node#isCompletelySpecified(List<ValidationError>)
* @see Leaf#isCompletelySpecified(List<ValidationError>)
* @return false
*/
public boolean isCompletelySpecified(List<ValidationError> errors) {
return false;
}
/**
* checks whether this node and its children are evaluated completely. This
* is overridden in Node and Leaf!
*
* @param errors
* TODO
* @see Node#isCompletelyEvaluated(List, List)
* @see Leaf#isCompletelyEvaluated(List, List)
* @return false
*/
public boolean isCompletelyEvaluated(List<Alternative> alternatives, List<ValidationError> errors) {
return false;
}
/**
* checks whether this node and its children have complete transformation
* settings. This is overridden in Node and Leaf!
*
* @param errors
* TODO
* @see Node#isCompletelyTransformed(List)
* @see Leaf#isCompletelyTransformed(List)
* @return false
*/
public boolean isCompletelyTransformed(List<ValidationError> errors) {
return false;
}
/**
* Checks whether this node is correctly weighted. This is overridden in
* Node and Leaf!
*
* @param errors
* TODO
* @see Node#isCorrectlyWeighted(List)
* @see Leaf#isCorrectlyWeighted(List)
* @return false
*/
public boolean isCorrectlyWeighted(List<ValidationError> errors) {
return false;
}
/**
* Touches every thing in the hierarchy: this treenode, all nodes and
* children down to the leaves, scales, transformers
*
* @see Leaf#touchAll()
*/
public void touchAll() {
touch();
for (TreeNode n : children) {
n.touchAll();
}
}
/**
* @see #touchAll()
*/
public void touchAll(String username) {
touch(username);
for (TreeNode n : children) {
n.touchAll(username);
}
}
public void touch() {
changeLog.touch();
}
public void touch(String username) {
changeLog.touch(username);
}
public boolean isChanged() {
return changeLog.isAltered();
}
public boolean isDirty() {
return changeLog.isDirty();
}
/**
* @see ITouchable#handleChanges(IChangesHandler)
*/
public void handleChanges(IChangesHandler h) {
// let the changeshandler handle this instance
h.visit(this);
// and call handleChanges of all child elements
for (TreeNode node : children) {
node.handleChanges(h);
}
}
/**
* returns a clone of self. Implemented for storing and inserting fragments.
* Subclasses obtain a shallow copy by invoking this method, then modifying
* the fields required to obtain a deep copy of this object.
*/
public TreeNode clone() {
try {
TreeNode clone = (TreeNode) super.clone();
clone.id = 0;
clone.setParent(null);
// created-timestamp is automatically set to now
clone.setChangeLog(new ChangeLog(this.getChangeLog().getChangedBy()));
if (this.getChildren() != null) {
List<TreeNode> clonedChildren = new ArrayList<TreeNode>();
for (TreeNode child : this.getChildren()) {
clonedChildren.add(child.clone());
}
clone.setChildren(clonedChildren);
}
return clone;
} catch (CloneNotSupportedException e) {
// Never thrown
return null;
}
}
public void convertToNode(Leaf l) {
Node node = new Node();
node.setName(l.getName());
node.setWeight(l.getWeight());
children.set(children.indexOf(l), node);
node.setParent(this);
}
public void convertToLeaf(Node n) {
Leaf leaf = new Leaf();
leaf.setName(n.getName());
leaf.setWeight(n.getWeight());
children.set(children.indexOf(n), leaf);
leaf.setParent(this);
}
public Set<TreeNode> getAllParents() {
Set<TreeNode> parents = null;
if (parent == null) {
parents = new HashSet<TreeNode>();
} else {
parents = parent.getAllParents();
parents.add(parent);
}
return parents;
}
/**
* Normalize the weights of this node.
*
* @see #normalizeWeights()
*
* @param recoursive
* If true, this function will be applied to this node and all
* its children recoursivly. Otherwise only this node will be
* normalized.
*/
public abstract void normalizeWeights(boolean recursive);
/**
* Make sure the weights are normalized. This means that all values are
* between 0 and 1 and the sum of all children of a tree node is equal 1.
*
* This function normalizes the weights of this node and is then applied to
* all children recoursivly.
*/
public void normalizeWeights() {
normalizeWeights(true);
}
public void walkTree(ITreeWalker treeWalker) {
treeWalker.walk(this);
for (TreeNode node : children) {
node.walkTree(treeWalker);
}
}
}