// 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* The root node of the tree not provided externally. *
*/ private TreeNode root = new TreeNode(); /** ** A map with all nodes currently known *
*/ private Map* 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 extends IEventTargetSpec> 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* 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* 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.
*
* 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* 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* 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* 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* 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* 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