// Copyright 2012 Georg-August-Universität Göttingen, Germany // // 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. package de.ugoe.cs.autoquest.eventcore; import java.io.OutputStream; import java.io.PrintStream; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.logging.Level; import de.ugoe.cs.autoquest.eventcore.IHierarchicalEventTargetModel; import de.ugoe.cs.util.console.Console; /** *

* A hierarchical event target model is a tree of {@link IHierarchicalEventTarget}s and represents * a complete user interface, e.g. GUI of a software. It is platform independent. It may have * several root nodes, as some user interfaces are made up of several frames (GUI) or scenes (VR) * being independent from each other. The event target model is filled using the * {@link #integratePath(List, IEventTargetFactory)} method. *

* * @version 1.0 * @author Patrick Harms, Steffen Herbold */ public class HierarchicalEventTargetModel implements Serializable, IHierarchicalEventTargetModel { /** */ private static final long serialVersionUID = 1L; /** *

* The root node of the tree not provided externally. *

*/ private TreeNode root = new TreeNode(); /** *

* A map with all nodes currently known *

*/ private Map allNodes = new HashMap(); /** *

* true, if internal validation is switched on, false else *

*/ private boolean validate = false; /** *

* Default constructor to create a event target model without internal validation *

* */ public HierarchicalEventTargetModel() { this(false); } /** *

* creates an event target model, that internally validates itself by checking on access to * nodes, if several event targets pretend to be equal or if several distinct event targets * have the same child. *

* * @param validate * true if internal validation shall be switched on (bad performance), false else * */ public HierarchicalEventTargetModel(boolean validate) { this.validate = validate; } /** *

* Integrates a path of event targets into the event target model. The event target model * itself is a tree and therefore a set of different paths through the tree that start with * a root node and end with a leaf node. Such a path can be added to the tree. The method * checks, if any of the event targets denoted by the path already exists. If so, it reuses it. * It may therefore also return an existing event target being the leaf node of the provided * path. If an event target of the path does not exist yet, it creates a new one using the * provided event target factory. *

*

* If an event target specification describes an existing event target or not is determined * through comparing the event target specifications of the existing event targets with the * ones provided in the path. The comparison is done using the * {@link IEventTargetSpec#getSimilarity(IEventTargetSpec)} method. The comparison is only done * on the correct levels. I.e. the currently known root elements of the tree are only compared * to the first element in the path. If the correct one is found or created, its children are * compared only to the second specification in the path, and so on. *

*

* The returned event targets are singletons. I.e. it is tried to return always the identical * object for the same denoted element. However, while creating the event target model, the * similarity of event targets may change. Therefore, the method might determine, that two * formerly different nodes are now similar. (This may happen, e.g. if event targets do not * have initial names which are set afterwards. Therefore, first they are handled differently * and later they can be identified as being the same.) In such a case, there are already * several event target objects instantiated for the same event target. The singleton paradigm * gets broken. Therefore, such event target objects are registered with each other, so that * their equal method can determine equality again correctly, although the objects are no * singletons anymore. *

* * @param eventTargetPath * the path to integrate into the model * @param eventTargetFactory * the event target factory to be used for instantiating event target objects * * @return The event target object representing the event target denoted by the provided path * * @throws EventTargetModelException * thrown in cases such as the event target object could not be instantiated * @throws IllegalArgumentException * if the provided path is invalid. */ public T integratePath(List eventTargetPath, IEventTargetFactory eventTargetFactory) throws EventTargetModelException, IllegalArgumentException { if ((eventTargetPath == null) || (eventTargetPath.size() <= 0)) { throw new IllegalArgumentException("event target path must contain at least one element"); } List remainingPath = new LinkedList(eventTargetPath); return (T) integratePath(root, remainingPath, eventTargetFactory); } /* (non-Javadoc) * @see IEventTargetModel#getChildren(IEventTarget) */ @Override public List getChildren(T eventTarget) { TreeNode node = findNode(eventTarget); List result = null; if (node != null) { result = new LinkedList(); if (node.children != null) { for (TreeNode child : node.children) { result.add((T) child.eventTarget); } } } else { boolean found = false; for (Map.Entry entry : allNodes.entrySet()) { if (entry.getKey().equals(eventTarget)) { if (!found) { System.out.println(eventTarget.hashCode() + " " + entry.getKey().hashCode()); found = true; } else { Console.traceln(Level.SEVERE, "Multiple nodes in the internal event " + "target model match the same event target. This should " + "not be the case and the event target model is probably " + "invalid."); } } } if (!found) { Console.traceln(Level.SEVERE, "event target belonging to model not found in model"); } } return result; } /* (non-Javadoc) * @see IEventTargetModel#getParent(IEventTarget) */ @Override public T getParent(T eventTarget) { T parent = null; for (Map.Entry entry : allNodes.entrySet()) { if (entry.getValue().children != null) { for (TreeNode child : entry.getValue().children) { if (child.eventTarget.equals(eventTarget)) { if (parent == null) { parent = entry.getKey(); if (!validate) { break; } } else { Console .traceln(Level.SEVERE, "Multiple nodes in the internal event target model match " + "the same event target. This should not be the case and the " + "event target model is probably invalid."); } } } } } return parent; } /* (non-Javadoc) * @see IEventTargetModel#getRootElements() */ @Override public List getRootElements() { List roots = new ArrayList(); if (root.children != null) { for (TreeNode rootChild : root.children) { roots.add(rootChild.eventTarget); } } return roots; } /** * returns a traverser for the event target model to have efficient access to the tree of event * targets without having direct access. * * @return a traverser */ @Override public Traverser getTraverser() { return new Traverser(); } /** * returns a traverser for the event target model starting at the given event target. Returns * null, if the event target is not part of the model. * * @return a traverser */ @Override public Traverser getTraverser(T startingAt) { TreeNode node = findNode(startingAt); if (node != null) { Traverser traverser = new Traverser(); traverser.navigateTo(node); return traverser; } else { return null; } } /** *

* dumps the event target model to the provided stream. Each node is represented through its * toString() method. If a node has children, those are dumped indented and surrounded by * braces. *

* * @param out * The stream to dump the textual representation of the model to * @param encoding * The encoding to be used while dumping */ public void dump(OutputStream out, String encoding) { PrintStream stream; if (out instanceof PrintStream) { stream = (PrintStream) out; } else { String enc = encoding == null ? "UTF-8" : encoding; try { stream = new PrintStream(out, true, enc); } catch (UnsupportedEncodingException e) { throw new IllegalArgumentException("encodind " + enc + " not supported"); } } for (TreeNode node : root.children) { dumpeventTarget(stream, node, ""); } } /** *

* This method groups the provided event targets under a common parent event target. The current * parent event target of the event targets to group must be the same. If the event targets to * be grouped are the whole list of children of the same parent, nothing is changed. *

* * @param eventTargets the list of event targets to be grouped * @param groupName the name of the event target group to be created * @param eventTargetFactory the event target factory used for creating a group of the correct * type (ensures, that all elements in the model can have T in their * parent hierarchy) * * @return the event target representing the group, or null, if the provided list of event * targets is empty * * @throws IllegalArgumentException * if not all event targets to be merged share the same parent, if one of the * parameters is null, or if one of the provided event targets does not belong to * the model */ public T groupEventTargets(List eventTargets, String groupName, IEventTargetFactory eventTargetFactory) throws IllegalArgumentException { if ((eventTargets == null) || (groupName == null) || (eventTargetFactory == null)) { throw new IllegalArgumentException("parameters must not be null"); } if (eventTargets.size() <= 0) { // do nothing return null; } TreeNode parent = findNode(eventTargets.get(0).getParent()); if (parent == null) { throw new IllegalArgumentException("event targets to group must have a parent: " + "parent of " + eventTargets.get(0) + " is " + eventTargets.get(0).getParent() + " and not found " + "in the model"); } List nodesToGroup = new LinkedList(); for (IHierarchicalEventTarget element : eventTargets) { if (!(element instanceof AbstractDefaultHierarchicalEventTarget)) { throw new IllegalArgumentException ("can only group nodes of type AbstractDefaultHierarchicalEventTarget"); } TreeNode node = findNode(element); if (node == null) { throw new IllegalArgumentException ("event target " + element + " is not part of the model"); } if (!nodesToGroup.contains(node)) { nodesToGroup.add(node); } TreeNode parentNode = findNode(element.getParent()); if (!parent.equals(parentNode)) { throw new IllegalArgumentException("event targets do not share the same parent: " + parent + " (parent of " + eventTargets.get(0) + ") <> " + parentNode + " (parent of " + element + ")"); } } TreeNode replacement = new TreeNode(); replacement.eventTarget = eventTargetFactory.instantiateGroup(groupName, parent.eventTarget, this); if (!(replacement.eventTarget instanceof HierarchicalEventTargetGroup)) { throw new IllegalArgumentException("factory does not instantiate an object of type " + "HierarchicalEventTargetGroup which is required " + "for performing the merge"); } for (TreeNode child : nodesToGroup) { ((HierarchicalEventTargetGroup) replacement.eventTarget).addToGroup(child.eventTarget); replacement.addChildNode(child); ((AbstractDefaultHierarchicalEventTarget) child.eventTarget).setParent (replacement.eventTarget); parent.children.remove(child); } parent.children.add(replacement); // finally, update the known nodes list // if you don't do this getChildren will return wrong things and very bad things happen! allNodes.put(replacement.eventTarget, replacement); return (T) replacement.eventTarget; } /** *

* By calling this method, the event target model is traversed and similar nodes are merged. *

* */ public void condenseModel() { mergeSubTree(root); } /** *

* Merges the tree nodes of two event targets. The event targets need to have the same parent. * They are merged recursively, i.e. also their children are merged. *

* * @param eventTarget1 * the first merge event target * @param eventTarget2 * the second merge event target * * @return the result of the merge * * @throws IllegalArgumentException * thrown if the two event targets do not have the same parent */ public T mergeEventTargets(T eventTarget1, T eventTarget2) throws IllegalArgumentException { return mergeEventTargets(eventTarget1, eventTarget2, true); } /** *

* Merges the tree nodes of two event targets. The event targets need to have the same parent. * If the recursively parameter is set to true, the children of the event targets * are merged, as well, as long as they are similar. If the parameter is false, the children * are not merged. In this case the resulting event target has all children of both merged * event targets. *

* * @param eventTarget1 * the first merge event target * @param eventTarget2 * the second merge event target * @param recursively * if true, the merge is done also for similar children, if false, not. * * @return the result of the merge * * @throws IllegalArgumentException * thrown if the two event targets do not have the same parent */ public T mergeEventTargets(T eventTarget1, T eventTarget2, boolean recursively) throws IllegalArgumentException { // check if both nodes have the same parent T parentElement = getParent(eventTarget1); boolean sameParent = (parentElement != null) ? parentElement.equals(eventTarget2.getParent()) : (eventTarget2.getParent() == null); if (!sameParent) { throw new IllegalArgumentException("can only merge nodes with the same parent"); } // get the TreeNode of the parent of the event targets TreeNode parent = findNode(parentElement); if ((parent == null) && (parentElement == null)) { // merging root nodes. The parent is the root node of the event target tree parent = root; } // get the TreeNodes for both event targets TreeNode node1 = findNode(eventTarget1); TreeNode node2 = findNode(eventTarget2); if (node1 == null || node2 == null) { throw new IllegalArgumentException ("Error while merging nodes: one element is not part of the event target model!"); } TreeNode replacement = mergeTreeNodes(node1, node2, recursively); if (parent != null) { // remove node1 and node2 from the parent's children and add the replacement instead // assumes that there are no duplicates of node1 and node2 if (parent.children != null) { parent.children.set(parent.children.indexOf(node1), replacement); parent.children.remove(node2); } } return replacement.eventTarget; } /** *

* internally integrates a path as the children of the provided parent node. This method is * recursive and calls itself, for the child of the parent node, that matches the first element * in the remaining path. *

* * @param parentNode * the parent node to add children for * @param eventTargetPath * the path of children to be created starting with the parent node * @param eventTargetFactory * the event target factory to be used for instantiating event target objects * * @return The event target object representing the event target denoted by the provided path * * @throws EventTargetModelException * thrown in cases such as the event target object could not be instantiated */ private T integratePath(TreeNode parentNode, List remainingPath, IEventTargetFactory eventTargetFactory) throws EventTargetModelException { IEventTargetSpec specToIntegrateElementFor = remainingPath.remove(0); TreeNode child = findEqualChild(parentNode, specToIntegrateElementFor); if (child == null) { T newElement = eventTargetFactory.instantiateEventTarget(specToIntegrateElementFor, parentNode.eventTarget); if (newElement instanceof AbstractDefaultHierarchicalEventTarget) { ((AbstractDefaultHierarchicalEventTarget) newElement).setEventTargetModel(this); } child = parentNode.addChild(newElement); allNodes.put(child.eventTarget, child); } if (remainingPath.size() > 0) { return integratePath(child, remainingPath, eventTargetFactory); } else { return child.eventTarget; } } /** *

* Searches the children of a tree node to see if the {@link IEventTargetSpec} equals the * specification of the {@link TreeNode#eventTarget} of the child. If a match is found, the * child is returned. *

* * @param parentNode * parent node whose children are searched * @param specToMatch * specification that is searched for * * @return matching child node or null if no child matches */ private TreeNode findEqualChild(TreeNode parentNode, IEventTargetSpec specToMatch) { if (parentNode.children != null) { for (TreeNode child : parentNode.children) { if (specToMatch.equals(child.eventTarget.getSpecification())) { return child; } } } return null; } /** *

* Merges all similar nodes in the sub-tree of the event target model defined by the * subTreeRoot. *

*

* The merging order is a bottom-up. This means, that we first call mergeSubTree recursively for * the grand children of the subTreeRoot, before we merge subTreeRoot. *

*

* The merging strategy is top-down. This means, that every time we merge two child nodes, we * call mergeSubTree recursively for all children of the merged nodes in order to check if we * can merge the children, too. *

* * @param subTreeRoot * root node of the sub-tree that is merged */ private void mergeSubTree(TreeNode subTreeRoot) { if (subTreeRoot.children == null || subTreeRoot.children.isEmpty()) { return; } // lets first merge the grand children of the parentNode for (TreeNode child : subTreeRoot.children) { mergeSubTree(child); } boolean performedMerge; do { performedMerge = false; for (int i = 0; !performedMerge && i < subTreeRoot.children.size(); i++) { IEventTargetSpec elemSpec1 = subTreeRoot.children.get(i).eventTarget.getSpecification(); for (int j = i + 1; !performedMerge && j < subTreeRoot.children.size(); j++) { IEventTargetSpec elemSpec2 = subTreeRoot.children.get(j).eventTarget.getSpecification(); if (elemSpec1.getSimilarity(elemSpec2)) { TreeNode replacement = mergeTreeNodes (subTreeRoot.children.get(i), subTreeRoot.children.get(j), true); subTreeRoot.children.set(i, replacement); subTreeRoot.children.remove(j); performedMerge = true; i--; break; } } } } while (performedMerge); } /** *

* merges two nodes with each other. Merging means registering the event target objects with * each other for equality checks. Further it adds all children of both nodes to a new * replacing node. Afterwards, all similar nodes of the replacement node are merged as well as * long the recursive parameter is set to true. *

* * @param treeNode1 * the first of the two nodes to be merged * @param treeNode2 * the second of the two nodes to be merged * @param recursively * if true, the merging also merges child nodes * * @return a tree node being the merge of the two provided nodes. */ private TreeNode mergeTreeNodes(TreeNode treeNode1, TreeNode treeNode2, boolean recursively) { // and now a replacement node that is the merge of treeNode1 and treeNode2 is created TreeNode replacement = new TreeNode(); replacement.eventTarget = treeNode1.eventTarget; if (treeNode1.children != null) { for (TreeNode child : treeNode1.children) { replacement.addChildNode(child); } } if (treeNode2.children != null) { for (TreeNode child : treeNode2.children) { replacement.addChildNode(child); } } if (recursively) { mergeSubTree(replacement); } replacement.eventTarget.updateSpecification(treeNode2.eventTarget.getSpecification()); // finally, update the known nodes list // if you don't do this getChildren will return wrong things and very bad things happen! allNodes.remove(treeNode1.eventTarget); allNodes.remove(treeNode2.eventTarget); // the following two lines are needed to preserve the references to the existing event // targets. If two elements are the same, one should be deleted to make the elements // singletons again. However, there may exist references to both objects. To preserve // these, we simply register the equal event targets with each other so that an equals // check can return true. treeNode1.eventTarget.addEqualEventTarget(treeNode2.eventTarget); treeNode2.eventTarget.addEqualEventTarget(treeNode1.eventTarget); allNodes.put(replacement.eventTarget, replacement); return replacement; } /** *

* dumps an event target to the stream. A dump contains the toString() representation of the * event target as well as an indented list of its children surrounded by braces. Therefore, * not the event target itself but its tree node is provided to have an efficient access to its * children *

* * @param out * {@link PrintStream} where the eventTarget is dumped to * @param node * the eventTarget's tree node of which the string representation is dumped * @param indent * indent string of the dumping */ private void dumpeventTarget(PrintStream out, TreeNode node, String indent) { out.print(indent); out.print(node.eventTarget); if ((node.children != null) && (node.children.size() > 0)) { out.println(" {"); for (TreeNode child : node.children) { dumpeventTarget(out, child, indent + " "); } out.print(indent); out.print("}"); } out.println(); } /** *

* Retrieves the TreeNode associated with an event target. Returns null if no such TreeNode is * found. *

* * @param element * the event target * * @return associated TreeNode; null if no such node exists */ private TreeNode findNode(IEventTarget element) { if (element == null) { return null; } TreeNode result = null; if (!validate) { result = allNodes.get(element); } else { for (Map.Entry entry : allNodes.entrySet()) { if (entry.getKey().equals(element)) { if (result == null) { result = entry.getValue(); } else { Console.traceln(Level.SEVERE, "Multiple nodes in the internal event " + "target model match the same event target. This should " + "not be the case and the event target model is probably " + "invalid."); } } } } return result; } /** *

* Used externally for tree traversal without providing direct access to the tree nodes *

* * @version 1.0 * @author Patrick Harms, Steffen Herbold */ private class Traverser implements IHierarchicalEventTargetModel.Traverser { /** *

* the stack of nodes on which the traverser currently works *

*/ private Stack nodeStack = new Stack(); /** *

* initializes the traverser by adding the root node of the event target model to the stack *

*/ private Traverser() { nodeStack.push(new StackEntry(root, 0)); } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.eventcore.guimodel.Traverser#firstChild() */ @Override public T firstChild() { return pushChild(0); } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.eventcore.guimodel.Traverser#hasFirstChild() */ @Override public boolean hasFirstChild() { return (nodeStack.peek().treeNode.children != null) && (nodeStack.peek().treeNode.children.size() > 0); } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.eventcore.guimodel.Traverser#nextSibling() */ @Override public T nextSibling() { int lastIndex = nodeStack.pop().index; T retval = pushChild(lastIndex + 1); if (retval == null) { pushChild(lastIndex); } return retval; } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.eventcore.guimodel.Traverser#hasNextSibling() */ @Override public boolean hasNextSibling() { boolean result = false; if (nodeStack.size() > 1) { StackEntry entry = nodeStack.pop(); result = nodeStack.peek().treeNode.children.size() > (entry.index + 1); pushChild(entry.index); } return result; } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.eventcore.guimodel.Traverser#parent() */ @Override public T parent() { T retval = null; if (nodeStack.size() > 1) { nodeStack.pop(); retval = nodeStack.peek().treeNode.eventTarget; } return retval; } /** *

* internal method used for changing the state of the traverser. I.e. to switch to a * specific child event target of the current one. *

*/ private T pushChild(int index) { T retVal = null; if ((nodeStack.peek().treeNode.children != null) && (nodeStack.peek().treeNode.children.size() > index)) { nodeStack.push (new StackEntry(nodeStack.peek().treeNode.children.get(index), index)); retVal = nodeStack.peek().treeNode.eventTarget; } return retVal; } /** *

* navigates the traverser to the given node in the event target model *

*/ private boolean navigateTo(TreeNode node) { if (hasFirstChild()) { T childElement = firstChild(); while (childElement != null) { if (childElement.equals(node.eventTarget)) { return true; } else if (navigateTo(node)) { return true; } else { childElement = nextSibling(); } } parent(); } return false; } /** *

* internal class needed to fill the stack with nodes of the event target models and their * respective index in the children of the parent node. *

*/ private class StackEntry { /** */ private TreeNode treeNode; /** */ private int index; /** *

* creates a new stack entry. *

*/ private StackEntry(TreeNode treeNode, int index) { this.treeNode = treeNode; this.index = index; } } } /** *

* Used internally for building up the tree of event targets. *

* * @version 1.0 * @author Patrick Harms, Steffen Herbold */ private class TreeNode implements Serializable { /** */ private static final long serialVersionUID = 1L; /** *

* event target associated with the TreeNode. *

*/ private T eventTarget; /** *

* Children of the TreeNode. *

*/ private List children; /** *

* Adds a child to the current node while keeping all lists of nodes up to date *

* * @param eventTarget * event target that will be associated with the new child * * @return the added child */ private TreeNode addChild(T eventTarget) { if (children == null) { children = new ArrayList(); } TreeNode child = new TreeNode(); child.eventTarget = eventTarget; children.add(child); return child; } /** * *

* Adds a TreeNode as child to the current node. This way, the whole sub-tree is added. *

* * @param node * child node that is added * * @return node that has been added */ private TreeNode addChildNode(TreeNode node) { if (children == null) { children = new ArrayList(); } children.add(node); return node; } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { return eventTarget.toString(); } } }