source: trunk/autoquest-core-events/src/main/java/de/ugoe/cs/autoquest/eventcore/guimodel/GUIModel.java @ 1086

Last change on this file since 1086 was 1086, checked in by pharms, 11 years ago
  • improved performance of showing GUI models
File size: 26.5 KB
Line 
1//   Copyright 2012 Georg-August-Universität Göttingen, Germany
2//
3//   Licensed under the Apache License, Version 2.0 (the "License");
4//   you may not use this file except in compliance with the License.
5//   You may obtain a copy of the License at
6//
7//       http://www.apache.org/licenses/LICENSE-2.0
8//
9//   Unless required by applicable law or agreed to in writing, software
10//   distributed under the License is distributed on an "AS IS" BASIS,
11//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//   See the License for the specific language governing permissions and
13//   limitations under the License.
14
15package de.ugoe.cs.autoquest.eventcore.guimodel;
16
17import java.io.OutputStream;
18import java.io.PrintStream;
19import java.io.UnsupportedEncodingException;
20import java.util.ArrayList;
21import java.util.LinkedList;
22import java.util.List;
23import java.util.Stack;
24import java.util.logging.Level;
25
26import de.ugoe.cs.util.console.Console;
27
28/**
29 * <p>
30 * A GUI model is a tree of {@link IGUIElements} and represents a complete GUI of a software. It is
31 * platform independent. It may have several root nodes, as some GUIs are made up of several Frames
32 * being independent from each other. The GUI model is filled using the
33 * {@link #integratePath(List, IGUIElementFactory)} method.
34 * </p>
35 *
36 * @version 1.0
37 * @author Patrick Harms, Steffen Herbold
38 */
39public class GUIModel {
40
41    /**
42     * <p>
43     * The root node of the tree not provided externally.
44     * </p>
45     */
46    private TreeNode root = new TreeNode();
47
48    /**
49     * <p>
50     * A list with all nodes currently known
51     * </p>
52     */
53    private List<TreeNode> allNodes = new ArrayList<TreeNode>();
54
55    /**
56     * <p>
57     * Integrates a path of GUI elements into the GUI model. The GUI model itself is a tree and
58     * therefore a set of different paths through the tree that start with a root node and end with
59     * a leaf node. Such a path can be added to the tree. The method checks, if any of the GUI
60     * elements denoted by the path already exists. If so, it reuses it. It may therefore also
61     * return an existing GUI element being the leaf node of the provided path. If a GUI element of
62     * the path does not exist yet, it creates a new one using the provided GUI element factory.
63     * </p>
64     * <p>
65     * If a GUI element specification describes an existing GUI element or not is determined through
66     * comparing the GUI element specifications of the existing GUI elements with the ones provided
67     * in the path. The comparison is done using the
68     * {@link IGUIElementSpec#getSimilarity(IGUIElementSpec)} method. The comparison is only done on
69     * the correct levels. I.e. the currently known root elements of the tree are only compared to
70     * the first element in the path. If the correct one is found or created, its children are
71     * compared only to the second specification in the path, and so on.
72     * </p>
73     * <p>
74     * The returned GUI elements are singletons. I.e. it is tried to return always the identical
75     * object for the same denoted element. However, while creating the GUI model, the similarity of
76     * GUI elements may change. Therefore, the method might determine, that two formerly different
77     * nodes are now similar. (This may happen, e.g. if GUI elements do not have initial names which
78     * are set afterwards. Therefore, first they are handled differently and later they can be
79     * identified as being the same.) In such a case, there are already several GUI element objects
80     * instantiated for the same GUI element. The singleton paradigm gets broken. Therefore, such
81     * GUI element objects are registered with each other, so that their equal method can determine
82     * equality again correctly, although the objects are no singletons anymore.
83     * </p>
84     *
85     * @param guiElementPath
86     *            the path to integrate into the model
87     * @param guiElementFactory
88     *            the GUI element factory to be used for instantiating GUI element objects
89     *
90     * @return The GUI element object representing the GUI element denoted by the provided path
91     *
92     * @throws GUIModelException
93     *             thrown in cases such as the GUI element object could not be instantiated
94     * @throws IllegalArgumentException
95     *             if the provided path is invalid.
96     */
97    public IGUIElement integratePath(List<? extends IGUIElementSpec> guiElementPath,
98                                     IGUIElementFactory guiElementFactory)
99        throws GUIModelException, IllegalArgumentException
100    {
101        if ((guiElementPath == null) || (guiElementPath.size() <= 0)) {
102            throw new IllegalArgumentException("GUI element path must contain at least one element");
103        }
104
105        List<IGUIElementSpec> remainingPath = new LinkedList<IGUIElementSpec>();
106
107        for (IGUIElementSpec spec : guiElementPath) {
108            remainingPath.add(spec);
109        }
110
111        return integratePath(root, remainingPath, guiElementFactory);
112    }
113
114    /**
115     * <p>
116     * Returns all children of the provided GUI element or null, if it does not have any or the node
117     * is unknown.
118     * </p>
119     *
120     * @param guiElement
121     *            the GUI element of which the children shall be returned
122     *
123     * @return As described
124     */
125    public List<IGUIElement> getChildren(IGUIElement guiElement) {
126        List<IGUIElement> result = null;
127        for (TreeNode node : allNodes) {
128            if (node.guiElement.equals(guiElement)) {
129                if (result == null) {
130                    result = new ArrayList<IGUIElement>();
131
132                    if (node.children != null) {
133                        for (TreeNode child : node.children) {
134                            result.add(child.guiElement);
135                        }
136                    }
137                }
138                else {
139                    Console
140                        .traceln(Level.SEVERE,
141                                 "Multiple nodes in the internal GUI model match the same GUI element. "
142                                     + "This should not be the case and the GUI model is probably invalid.");
143                }
144            }
145        }
146
147        return result;
148    }
149
150    /**
151     * <p>
152     * Returns the parent GUI element of the provided GUI element or null, if it does not have a
153     * parent (i.e. if it is a root node) or if the node is unknown.
154     * </p>
155     *
156     * @param guiElement
157     *            the GUI element of which the parent shall be returned
158     *
159     * @return As described
160     */
161    public IGUIElement getParent(IGUIElement guiElement) {
162        IGUIElement parent = null;
163
164        for (TreeNode node : allNodes) {
165            for (TreeNode child : node.children) {
166                if (child.guiElement.equals(guiElement)) {
167                    if (parent != null) {
168                        parent = node.guiElement;
169                    }
170                    else {
171                        Console
172                            .traceln(Level.SEVERE,
173                                     "Multiple nodes in the internal GUI model match the same GUI element. "
174                                         + "This should not be the case and the GUI model is probably invalid.");
175                    }
176                }
177            }
178        }
179
180        return parent;
181    }
182
183    /**
184     * <p>
185     * Returns all root GUI elements of the model or an empty list, if the model is empty
186     * </p>
187     *
188     * @return As described
189     */
190    public List<IGUIElement> getRootElements() {
191        List<IGUIElement> roots = new ArrayList<IGUIElement>();
192
193        if (root.children != null) {
194            for (TreeNode rootChild : root.children) {
195                roots.add(rootChild.guiElement);
196            }
197        }
198
199        return roots;
200    }
201   
202    /**
203     * returns a traverser for the GUI model to have efficient access to the tree of GUI elements
204     * without having direct access.
205     *
206     * @return a traverser
207     */
208    public Traverser getTraverser() {
209        return new Traverser();
210    }
211
212    /**
213     * <p>
214     * dumps the GUI model to the provided stream. Each node is represented through its toString()
215     * method. If a node has children, those are dumped indented and surrounded by braces.
216     * </p>
217     *
218     * @param out
219     *            The stream to dump the textual representation of the model to
220     * @param encoding
221     *            The encoding to be used while dumping
222     */
223    public void dump(OutputStream out, String encoding) {
224        PrintStream stream;
225
226        if (out instanceof PrintStream) {
227            stream = (PrintStream) out;
228        }
229        else {
230            String enc = encoding == null ? "UTF-8" : encoding;
231            try {
232                stream = new PrintStream(out, true, enc);
233            }
234            catch (UnsupportedEncodingException e) {
235                throw new IllegalArgumentException("encodind " + enc + " not supported");
236            }
237        }
238
239        for (TreeNode node : root.children) {
240            dumpGUIElement(stream, node, "");
241        }
242    }
243
244    /**
245     * <p>
246     * By calling this method, the GUIModel is traversed and similar nodes are merged.
247     * </p>
248     *
249     */
250    public void condenseModel() {
251        mergeSubTree(root);
252    }
253   
254    /**
255     * <p>
256     * Merges the tree nodes of two GUI elements. The GUI elements need to have the same parent.
257     * </p>
258     *
259     * @param guiElement1
260     *            the first merge GUI element
261     * @param guiElement2
262     *            the second merge GUI element
263     * @throws IllegalArgumentException
264     *             thrown if the two GUI elements do not have the same parent
265     */
266    public void mergeGUIElements(IGUIElement guiElement1, IGUIElement guiElement2)
267        throws IllegalArgumentException
268    {
269        // check if both nodes have the same parent
270        IGUIElement parentElement = guiElement1.getParent();
271        if (parentElement != null && !parentElement.equals(guiElement2.getParent())) {
272            throw new IllegalArgumentException("can only merge nodes with the same parent");
273        }
274
275        // get the TreeNode of the parent of the GUI elements
276        TreeNode parent = findNode(parentElement);
277
278        // get the TreeNodes for both GUI elements
279        TreeNode node1 = findNode(guiElement1);
280        TreeNode node2 = findNode(guiElement2);
281
282        if (node1 == null || node2 == null) {
283            throw new IllegalArgumentException(
284                                               "Error while merging nodes: one element is not part of the GUI model!");
285        }
286
287        TreeNode replacement = mergeTreeNodes(node1, node2);
288
289        if (parent != null) {
290            // remove node1 and node2 from the parent's children and add the replacement instead
291            // assumes that there are no duplicates of node1 and node2
292            if (parent.children != null) {
293                parent.children.set(parent.children.indexOf(node1), replacement);
294                parent.children.remove(node2);
295            }
296        }
297
298    }
299
300    /**
301     * <p>
302     * internally integrates a path as the children of the provided parent node. This method is
303     * recursive and calls itself, for the child of the parent node, that matches the first element
304     * in the remaining path.
305     * </p>
306     *
307     * @param parentNode
308     *            the parent node to add children for
309     * @param guiElementPath
310     *            the path of children to be created starting with the parent node
311     * @param guiElementFactory
312     *            the GUI element factory to be used for instantiating GUI element objects
313     *
314     * @return The GUI element object representing the GUI element denoted by the provided path
315     *
316     * @throws GUIModelException
317     *             thrown in cases such as the GUI element object could not be instantiated
318     */
319    private IGUIElement integratePath(TreeNode parentNode,
320                                      List<? extends IGUIElementSpec> remainingPath,
321                                      IGUIElementFactory guiElementFactory)
322        throws GUIModelException
323    {
324        IGUIElementSpec specToIntegrateElementFor = remainingPath.remove(0);
325
326        TreeNode child = findEqualChild(parentNode, specToIntegrateElementFor);
327        if (child == null) {
328            IGUIElement newElement =
329                guiElementFactory.instantiateGUIElement(specToIntegrateElementFor,
330                                                        parentNode.guiElement);
331
332            child = parentNode.addChild(newElement);
333        }
334
335        if (remainingPath.size() > 0) {
336            return integratePath(child, remainingPath, guiElementFactory);
337        }
338        else {
339            return child.guiElement;
340        }
341    }
342
343    /**
344     * <p>
345     * Searches the children of a tree node to see if the {@link IGUIElementSpec} of equals the
346     * specification of the {@link TreeNode#guiElement} of the child. If a match is found, the child
347     * is returned.
348     * </p>
349     *
350     * @param parentNode
351     *            parent node whose children are searched
352     * @param specToMatch
353     *            specification that is searched for
354     * @return matching child node or null if no child matches
355     */
356    private TreeNode findEqualChild(TreeNode parentNode, IGUIElementSpec specToMatch) {
357        if (parentNode.children != null) {
358            for (TreeNode child : parentNode.children) {
359                if (specToMatch.equals(child.guiElement.getSpecification())) {
360                    return child;
361                }
362            }
363        }
364        return null;
365    }
366
367    /**
368     * <p>
369     * Merges all similar nodes in the sub-tree of the GUI model defined by the subTreeRoot.
370     * </p>
371     * <p>
372     * The merging order is a bottom-up. This means, that we first call mergeSubTree recursively for
373     * the grand children of the subTreeRoot, before we merge subTreeRoot.
374     * </p>
375     * <p>
376     * The merging strategy is top-down. This means, that every time we merge two child nodes, we
377     * call mergeSubTree recursively for all children of the merged nodes in order to check if we
378     * can merge the children, too.
379     * </p>
380     *
381     * @param subTreeRoot
382     *            root node of the sub-tree that is merged
383     */
384    private void mergeSubTree(TreeNode subTreeRoot) {
385        if (subTreeRoot.children == null || subTreeRoot.children.isEmpty()) {
386            return;
387        }
388
389        // lets first merge the grand children of the parentNode
390        for (TreeNode child : subTreeRoot.children) {
391            mergeSubTree(child);
392        }
393
394        boolean performedMerge;
395
396        do {
397            performedMerge = false;
398            for (int i = 0; !performedMerge && i < subTreeRoot.children.size(); i++) {
399                IGUIElementSpec elemSpec1 =
400                    subTreeRoot.children.get(i).guiElement.getSpecification();
401                for (int j = i + 1; !performedMerge && j < subTreeRoot.children.size(); j++) {
402                    IGUIElementSpec elemSpec2 =
403                        subTreeRoot.children.get(j).guiElement.getSpecification();
404                    if (elemSpec1.getSimilarity(elemSpec2)) {
405                        TreeNode replacement =
406                            mergeTreeNodes(subTreeRoot.children.get(i), subTreeRoot.children.get(j));
407
408                        subTreeRoot.children.set(i, replacement);
409                        subTreeRoot.children.remove(j);
410                        performedMerge = true;
411                        i--;
412                        break;
413                    }
414                }
415            }
416        }
417        while (performedMerge);
418    }
419
420    /**
421     * <p>
422     * merges two nodes with each other. Merging means registering the GUI element objects with each
423     * other for equality checks. Further it add all children of both nodes to a new replacing node.
424     * Afterwards, all similar nodes of the replacement node are merged as well.
425     * </p>
426     *
427     * @param treeNode1
428     *            the first of the two nodes to be merged
429     * @param treeNode2
430     *            the second of the two nodes to be merged
431     * @return a tree node being the merge of the two provided nodes.
432     */
433    private TreeNode mergeTreeNodes(TreeNode treeNode1, TreeNode treeNode2) {
434        // the following two lines are needed to preserve the references to the existing GUI
435        // elements. If two elements are the same, one should be deleted to make the elements
436        // singletons again. However, there may exist references to both objects. To preserve
437        // these, we simply register the equal GUI elements with each other so that an equals
438        // check can return true.
439        treeNode1.guiElement.addEqualGUIElement(treeNode2.guiElement);
440        treeNode2.guiElement.addEqualGUIElement(treeNode1.guiElement);
441
442        // and now a replacement node that is the merge of treeNode1 and treeNode2 is created
443        TreeNode replacement = new TreeNode();
444        replacement.guiElement = treeNode1.guiElement;
445        if (treeNode1.children != null) {
446            for (TreeNode child : treeNode1.children) {
447                replacement.addChildNode(child);
448            }
449        }
450        if (treeNode2.children != null) {
451            for (TreeNode child : treeNode2.children) {
452                replacement.addChildNode(child);
453            }
454        }
455
456        mergeSubTree(replacement);
457
458        replacement.guiElement.updateSpecification(treeNode2.guiElement.getSpecification());
459
460        // finally, update the known nodes list
461        // if you don't do this getChildren will return wrong things and very bad things happen!
462        allNodes.remove(treeNode1);
463        allNodes.remove(treeNode2);
464        allNodes.add(replacement);
465
466        return replacement;
467    }
468
469    /**
470     * <p>
471     * dumps a GUI element to the stream. A dump contains the toString() representation of the GUI
472     * element as well as a indented list of its children surrounded by braces. Therefore, not the
473     * GUI element itself but its tree node is provided to have an efficient access to its children
474     * </p>
475     *
476     * @param out
477     *            {@link PrintStream} where the guiElement is dumped to
478     * @param node
479     *            the guiElement's tree node of which the string representation is dumped
480     * @param indent
481     *            indent string of the dumping
482     */
483    private void dumpGUIElement(PrintStream out, TreeNode node, String indent) {
484        out.print(indent);
485        out.print(node.guiElement);
486
487        if ((node.children != null) && (node.children.size() > 0)) {
488            out.println(" {");
489
490            for (TreeNode child : node.children) {
491                dumpGUIElement(out, child, indent + "  ");
492            }
493
494            out.print(indent);
495            out.print("}");
496        }
497
498        out.println();
499    }
500   
501    /**
502     * <p>
503     * Retrieves the TreeNode associated with a GUI element. Returns null if no such TreeNode is
504     * found.
505     * </p>
506     *
507     * @param element
508     *            the GUI element
509     * @return associated TreeNode; null if no such node exists
510     */
511    private TreeNode findNode(IGUIElement element) {
512        if (element == null) {
513            return null;
514        }
515
516        TreeNode result = null;
517        for (TreeNode node : allNodes) {
518            if (node.guiElement.equals(element)) {
519                if (result == null) {
520                    result = node;
521                }
522                else {
523                    Console
524                        .traceln(Level.SEVERE,
525                                 "Multiple nodes in the internal GUI model match the same GUI element. "
526                                     + "This should not be the case and the GUI model is probably invalid.");
527                }
528            }
529        }
530        return result;
531    }
532
533    /**
534     * <p>
535     * Used externally for tree traversal without providing direct access to the tree nodes
536     * </p>
537     *
538     * @version 1.0
539     * @author Patrick Harms, Steffen Herbold
540     */
541    public class Traverser {
542       
543        /**
544         * <p>
545         * the stack of nodes on which the traverser currently works
546         * </p>
547         */
548        private Stack<StackEntry> nodeStack = new Stack<StackEntry>();
549       
550        /**
551         * <p>
552         * initializes the traverser by adding the root node of the GUI model to the stack
553         * </p>
554         */
555        private Traverser() {
556            nodeStack.push(new StackEntry(root, 0));
557        }
558       
559        /**
560         * <p>
561         * returns the first child of the current GUI element. On the first call of this method on
562         * the traverser the first of the root GUI elements of the GUI model is returned. If the
563         * current GUI element does not have children, the method returns null. If the GUI model
564         * is empty, then a call to this method will return null. The returned GUI element is the
565         * next one the traverser points to.
566         * </p>
567         *
568         * @return as described.
569         */
570        public IGUIElement firstChild() {
571            return pushChild(0);
572        }
573       
574        /**
575         * <p>
576         * returns true, if the current GUI element has a first child, i.e. if the next call to the
577         * method {@link #firstChild()} would return a GUI element or null.
578         * </p>
579         *
580         * @return as described
581         */
582        public boolean hasFirstChild() {
583            return
584                (nodeStack.peek().treeNode.children != null) &&
585                (nodeStack.peek().treeNode.children.size() > 0);
586        }
587       
588        /**
589         * <p>
590         * returns the next sibling of the current GUI element. If there is no further sibling,
591         * null is returned. If the current GUI element is one of the root nodes, the next root
592         * node of the GUI model is returned. The returned GUI element is the next one the
593         * traverser points to.
594         * </p>
595         *
596         * @return as described
597         */
598        public IGUIElement nextSibling() {
599            int lastIndex = nodeStack.pop().index;
600           
601            IGUIElement retval = pushChild(lastIndex + 1);
602            if (retval == null) {
603                pushChild(lastIndex);
604            }
605           
606            return retval;
607        }
608       
609        /**
610         * <p>
611         * returns true, if the current GUI element has a further sibling, i.e. if a call to the
612         * method {@link #nextSibling()} will return a GUI element;
613         * </p>
614         *
615         * @return as described
616         */
617        public boolean hasNextSibling() {
618            StackEntry entry = nodeStack.pop();
619            boolean result = nodeStack.peek().treeNode.children.size() > (entry.index + 1);
620            pushChild(entry.index);
621            return result;
622        }
623       
624        /**
625         * <p>
626         * returns the parent GUI element of the current GUI element. If the current GUI element
627         * is a root node, null is returned. If there is no current GUI element yet as the method
628         * {@link #firstChild()} was not called yet, null is returned.
629         * </p>
630         *
631         * @return as described
632         */
633        public IGUIElement parent() {
634            IGUIElement retval = null;
635           
636            if (nodeStack.size() > 1) {
637                nodeStack.pop();
638                retval = nodeStack.peek().treeNode.guiElement;
639            }
640           
641            return retval;
642        }
643       
644        /**
645         * <p>
646         * internal method used for changing the state of the traverser. I.e. to switch to a
647         * specific child GUI element of the current one.
648         * </p>
649         */
650        private IGUIElement pushChild(int index) {
651            IGUIElement retVal = null;
652           
653            if ((nodeStack.peek().treeNode.children != null) &&
654                (nodeStack.peek().treeNode.children.size() > index))
655            {
656                nodeStack.push
657                    (new StackEntry(nodeStack.peek().treeNode.children.get(index), index));
658                retVal = nodeStack.peek().treeNode.guiElement;
659            }
660           
661            return retVal;
662        }
663       
664        /**
665         * <p>
666         * internal class needed to fill the stack with nodes of the GUI models and their
667         * respective index in the children of the parent node.
668         * </p>
669         */
670        private class StackEntry {
671           
672            /** */
673            private TreeNode treeNode;
674           
675            /** */
676            private int index;
677           
678            /**
679             * <p>
680             * creates a new stack entry.
681             * </p>
682             */
683            private StackEntry(TreeNode treeNode, int index) {
684                this.treeNode = treeNode;
685                this.index = index;
686            }
687        }
688    }
689
690    /**
691     * <p>
692     * Used internally for building up the tree of GUI elements.
693     * </p>
694     *
695     * @version 1.0
696     * @author Patrick Harms, Steffen Herbold
697     */
698    private class TreeNode {
699
700        /**
701         * <p>
702         * GUI element associated with the TreeNode.
703         * </p>
704         */
705        private IGUIElement guiElement;
706
707        /**
708         * <p>
709         * Children of the TreeNode.
710         * </p>
711         */
712        private List<TreeNode> children;
713
714        /**
715         * <p>
716         * Adds a child to the current node while keeping all lists of nodes up to date
717         * </p>
718         *
719         * @param guiElement
720         *            GUI element that will be associated with the new child
721         * @return the added child
722         */
723        private TreeNode addChild(IGUIElement guiElement) {
724            if (children == null) {
725                children = new ArrayList<TreeNode>();
726            }
727
728            TreeNode child = new TreeNode();
729            child.guiElement = guiElement;
730            children.add(child);
731
732            allNodes.add(child);
733
734            return child;
735        }
736
737        /**
738         *
739         * <p>
740         * Adds a TreeNode as child to the current node. This way, the whole sub-tree is added.
741         * </p>
742         *
743         * @param node
744         *            child node that is added
745         * @return node that has been added
746         */
747        private TreeNode addChildNode(TreeNode node) {
748            if (children == null) {
749                children = new ArrayList<TreeNode>();
750            }
751            children.add(node);
752            return node;
753        }
754
755        /*
756         * (non-Javadoc)
757         *
758         * @see java.lang.Object#toString()
759         */
760        @Override
761        public String toString() {
762            return guiElement.toString();
763        }
764    }
765}
Note: See TracBrowser for help on using the repository browser.