// 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.guimodel; 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.util.console.Console; /** *
* A GUI model is a tree of {@link IGUIElements} and represents a complete GUI of a software. It is * platform independent. It may have several root nodes, as some GUIs are made up of several Frames * being independent from each other. The GUI model is filled using the * {@link #integratePath(List, IGUIElementFactory)} method. *
* * @version 1.0 * @author Patrick Harms, Steffen Herbold */ public class GUIModel implements Serializable { /** */ 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* true, if internal validation is switched on, false else *
*/ private boolean validate = false; /** ** Default constructor to create a GUI model without internal validation *
* */ public GUIModel() { this(false); } /** ** creates a GUI model, that internally validates itself by checking on access to nodes, * if several GUI elements pretend to be equal or if several distinct GUI elements have the * same child. *
* * @param validate * true if internal validation shall be switched on (bad performance), false else * */ public GUIModel(boolean validate) { this.validate = validate; } /** ** Integrates a path of GUI elements into the GUI model. The GUI 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 GUI * elements denoted by the path already exists. If so, it reuses it. It may therefore also * return an existing GUI element being the leaf node of the provided path. If a GUI element of * the path does not exist yet, it creates a new one using the provided GUI element factory. *
** If a GUI element specification describes an existing GUI element or not is determined through * comparing the GUI element specifications of the existing GUI elements with the ones provided * in the path. The comparison is done using the * {@link IGUIElementSpec#getSimilarity(IGUIElementSpec)} 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 GUI elements are singletons. I.e. it is tried to return always the identical * object for the same denoted element. However, while creating the GUI model, the similarity of * GUI elements may change. Therefore, the method might determine, that two formerly different * nodes are now similar. (This may happen, e.g. if GUI elements 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 GUI element objects * instantiated for the same GUI element. The singleton paradigm gets broken. Therefore, such * GUI element objects are registered with each other, so that their equal method can determine * equality again correctly, although the objects are no singletons anymore. *
* * @param guiElementPath * the path to integrate into the model * @param guiElementFactory * the GUI element factory to be used for instantiating GUI element objects * * @return The GUI element object representing the GUI element denoted by the provided path * * @throws GUIModelException * thrown in cases such as the GUI element object could not be instantiated * @throws IllegalArgumentException * if the provided path is invalid. */ public IGUIElement integratePath(List extends IGUIElementSpec> guiElementPath, IGUIElementFactory guiElementFactory) throws GUIModelException, IllegalArgumentException { if ((guiElementPath == null) || (guiElementPath.size() <= 0)) { throw new IllegalArgumentException("GUI element path must contain at least one element"); } List* Returns all children of the provided GUI element or null, if it does not have any or the node * is unknown. *
* * @param guiElement * the GUI element of which the children shall be returned * * @return As described */ public List* Returns the parent GUI element of the provided GUI element or null, if it does not have a * parent (i.e. if it is a root node) or if the node is unknown. *
* * @param guiElement * the GUI element of which the parent shall be returned * * @return As described */ public IGUIElement getParent(IGUIElement guiElement) { IGUIElement parent = null; for (Map.Entry* Returns all root GUI elements of the model or an empty list, if the model is empty *
* * @return As described */ public List* dumps the GUI 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) { dumpGUIElement(stream, node, ""); } } /** ** This method groups the provided GUI elements under a common parent GUI element. The current * parent GUI element of the GUI elements to group must be the same. If the GUI elements to * be grouped are the whole list of children of the same parent, nothing is changed. *
* * @param guiElements the list of GUI elements to be grouped * @param groupName the name of the GUI element group to be created * * @return the GUI element representing the group, or null, if the provided list of GUI elements * is empty * * @throws IllegalArgumentException * if not all GUI elements to be merged share the same parent, if one of the * parameters is null, or if one of the provided GUI elements does not belong to * the model */ public IGUIElement groupGUIElements(List* By calling this method, the GUIModel is traversed and similar nodes are merged. *
* */ public void condenseModel() { mergeSubTree(root); } /** ** Merges the tree nodes of two GUI elements. The GUI elements need to have the same parent. * They are merged recursively, i.e. also their children are merged. *
* * @param guiElement1 * the first merge GUI element * @param guiElement2 * the second merge GUI element * * @return the result of the merge * * @throws IllegalArgumentException * thrown if the two GUI elements do not have the same parent */ public IGUIElement mergeGUIElements(IGUIElement guiElement1, IGUIElement guiElement2) throws IllegalArgumentException { return mergeGUIElements(guiElement1, guiElement2, true); } /** *
* Merges the tree nodes of two GUI elements. The GUI elements need to have the same parent.
* If the recursively
parameter is set to true, the children of the GUI elements
* 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 GUI element has all children of both merged GUI
* elements.
*
* 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 guiElementPath * the path of children to be created starting with the parent node * @param guiElementFactory * the GUI element factory to be used for instantiating GUI element objects * * @return The GUI element object representing the GUI element denoted by the provided path * * @throws GUIModelException * thrown in cases such as the GUI element object could not be instantiated */ private IGUIElement integratePath(TreeNode parentNode, List extends IGUIElementSpec> remainingPath, IGUIElementFactory guiElementFactory) throws GUIModelException { IGUIElementSpec specToIntegrateElementFor = remainingPath.remove(0); TreeNode child = findEqualChild(parentNode, specToIntegrateElementFor); if (child == null) { IGUIElement newElement = guiElementFactory.instantiateGUIElement(specToIntegrateElementFor, parentNode.guiElement); if (newElement instanceof AbstractDefaultGUIElement) { ((AbstractDefaultGUIElement) newElement).setGUIModel(this); } child = parentNode.addChild(newElement); allNodes.put(child.guiElement, child); } if (remainingPath.size() > 0) { return integratePath(child, remainingPath, guiElementFactory); } else { return child.guiElement; } } /** ** Searches the children of a tree node to see if the {@link IGUIElementSpec} of equals the * specification of the {@link TreeNode#guiElement} 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, IGUIElementSpec specToMatch) { if (parentNode.children != null) { for (TreeNode child : parentNode.children) { if (specToMatch.equals(child.guiElement.getSpecification())) { return child; } } } return null; } /** ** Merges all similar nodes in the sub-tree of the GUI 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++) { IGUIElementSpec elemSpec1 = subTreeRoot.children.get(i).guiElement.getSpecification(); for (int j = i + 1; !performedMerge && j < subTreeRoot.children.size(); j++) { IGUIElementSpec elemSpec2 = subTreeRoot.children.get(j).guiElement.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 GUI element 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.guiElement = treeNode1.guiElement; 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.guiElement.updateSpecification(treeNode2.guiElement.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.guiElement); allNodes.remove(treeNode2.guiElement); // the following two lines are needed to preserve the references to the existing GUI // elements. 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 GUI elements with each other so that an equals // check can return true. treeNode1.guiElement.addEqualGUIElement(treeNode2.guiElement); treeNode2.guiElement.addEqualGUIElement(treeNode1.guiElement); allNodes.put(replacement.guiElement, replacement); return replacement; } /** ** dumps a GUI element to the stream. A dump contains the toString() representation of the GUI * element as well as a indented list of its children surrounded by braces. Therefore, not the * GUI element itself but its tree node is provided to have an efficient access to its children *
* * @param out * {@link PrintStream} where the guiElement is dumped to * @param node * the guiElement's tree node of which the string representation is dumped * @param indent * indent string of the dumping */ private void dumpGUIElement(PrintStream out, TreeNode node, String indent) { out.print(indent); out.print(node.guiElement); if ((node.children != null) && (node.children.size() > 0)) { out.println(" {"); for (TreeNode child : node.children) { dumpGUIElement(out, child, indent + " "); } out.print(indent); out.print("}"); } out.println(); } /** ** Retrieves the TreeNode associated with a GUI element. Returns null if no such TreeNode is * found. *
* * @param element * the GUI element * @return associated TreeNode; null if no such node exists */ private TreeNode findNode(IGUIElement 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 */ public class Traverser { /** ** the stack of nodes on which the traverser currently works *
*/ private Stack* initializes the traverser by adding the root node of the GUI model to the stack *
*/ private Traverser() { nodeStack.push(new StackEntry(root, 0)); } /** ** returns the first child of the current GUI element. On the first call of this method on * the traverser the first of the root GUI elements of the GUI model is returned. If the * current GUI element does not have children, the method returns null. If the GUI model * is empty, then a call to this method will return null. The returned GUI element is the * next one the traverser points to. *
* * @return as described. */ public IGUIElement firstChild() { return pushChild(0); } /** ** returns true, if the current GUI element has a first child, i.e. if the next call to the * method {@link #firstChild()} would return a GUI element or null. *
* * @return as described */ public boolean hasFirstChild() { return (nodeStack.peek().treeNode.children != null) && (nodeStack.peek().treeNode.children.size() > 0); } /** ** returns the next sibling of the current GUI element. If there is no further sibling, * null is returned. If the current GUI element is one of the root nodes, the next root * node of the GUI model is returned. The returned GUI element is the next one the * traverser points to. *
* * @return as described */ public IGUIElement nextSibling() { int lastIndex = nodeStack.pop().index; IGUIElement retval = pushChild(lastIndex + 1); if (retval == null) { pushChild(lastIndex); } return retval; } /** ** returns true, if the current GUI element has a further sibling, i.e. if a call to the * method {@link #nextSibling()} will return a GUI element; *
* * @return as described */ 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; } /** ** returns the parent GUI element of the current GUI element. If the current GUI element * is a root node, null is returned. If there is no current GUI element yet as the method * {@link #firstChild()} was not called yet, null is returned. *
* * @return as described */ public IGUIElement parent() { IGUIElement retval = null; if (nodeStack.size() > 1) { nodeStack.pop(); retval = nodeStack.peek().treeNode.guiElement; } return retval; } /** ** internal method used for changing the state of the traverser. I.e. to switch to a * specific child GUI element of the current one. *
*/ private IGUIElement pushChild(int index) { IGUIElement 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.guiElement; } return retVal; } /** ** navigates the traverser to the given node in the GUI model *
*/ private boolean navigateTo(TreeNode node) { if (hasFirstChild()) { IGUIElement childElement = firstChild(); while (childElement != null) { if (childElement.equals(node.guiElement)) { 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 GUI 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 GUI elements. *
* * @version 1.0 * @author Patrick Harms, Steffen Herbold */ private static class TreeNode implements Serializable { /** */ private static final long serialVersionUID = 1L; /** ** GUI element associated with the TreeNode. *
*/ private IGUIElement guiElement; /** ** Children of the TreeNode. *
*/ private List* Adds a child to the current node while keeping all lists of nodes up to date *
* * @param guiElement * GUI element that will be associated with the new child * @return the added child */ private TreeNode addChild(IGUIElement guiElement) { 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