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

Last change on this file since 927 was 927, checked in by sherbold, 12 years ago
  • added copyright under the Apache License, Version 2.0
File size: 21.1 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.logging.Level;
24
25import de.ugoe.cs.util.console.Console;
26
27/**
28 * <p>
29 * A GUI model is a tree of {@link IGUIElements} and represents a complete GUI of a software. It is
30 * platform independent. It may have several root nodes, as some GUIs are made up of several Frames
31 * being independent from each other. The GUI model is filled using the
32 * {@link #integratePath(List, IGUIElementFactory)} method.
33 * </p>
34 *
35 * @version 1.0
36 * @author Patrick Harms, Steffen Herbold
37 */
38public class GUIModel {
39
40    /**
41     * <p>
42     * The root node of the tree not provided externally.
43     * </p>
44     */
45    private TreeNode root = new TreeNode();
46
47    /**
48     * <p>
49     * A list with all nodes currently known
50     * </p>
51     */
52    private List<TreeNode> allNodes = new ArrayList<TreeNode>();
53
54    /**
55     * <p>
56     * Integrates a path of GUI elements into the GUI model. The GUI model itself is a tree and
57     * therefore a set of different paths through the tree that start with a root node and end with
58     * a leaf node. Such a path can be added to the tree. The method checks, if any of the GUI
59     * elements denoted by the path already exists. If so, it reuses it. It may therefore also
60     * return an existing GUI element being the leaf node of the provided path. If a GUI element of
61     * the path does not exist yet, it creates a new one using the provided GUI element factory.
62     * </p>
63     * <p>
64     * If a GUI element specification describes an existing GUI element or not is determined through
65     * comparing the GUI element specifications of the existing GUI elements with the ones provided
66     * in the path. The comparison is done using the
67     * {@link IGUIElementSpec#getSimilarity(IGUIElementSpec)} method. The comparison is only done on
68     * the correct levels. I.e. the currently known root elements of the tree are only compared to
69     * the first element in the path. If the correct one is found or created, its children are
70     * compared only to the second specification in the path, and so on.
71     * </p>
72     * <p>
73     * The returned GUI elements are singletons. I.e. it is tried to return always the identical
74     * object for the same denoted element. However, while creating the GUI model, the similarity of
75     * GUI elements may change. Therefore, the method might determine, that two formerly different
76     * nodes are now similar. (This may happen, e.g. if GUI elements do not have initial names which
77     * are set afterwards. Therefore, first they are handled differently and later they can be
78     * identified as being the same.) In such a case, there are already several GUI element objects
79     * instantiated for the same GUI element. The singleton paradigm gets broken. Therefore, such
80     * GUI element objects are registered with each other, so that their equal method can determine
81     * equality again correctly, although the objects are no singletons anymore.
82     * </p>
83     *
84     * @param guiElementPath
85     *            the path to integrate into the model
86     * @param guiElementFactory
87     *            the GUI element factory to be used for instantiating GUI element objects
88     *
89     * @return The GUI element object representing the GUI element denoted by the provided path
90     *
91     * @throws GUIModelException
92     *             thrown in cases such as the GUI element object could not be instantiated
93     * @throws IllegalArgumentException
94     *             if the provided path is invalid.
95     */
96    public IGUIElement integratePath(List<? extends IGUIElementSpec> guiElementPath,
97                                     IGUIElementFactory guiElementFactory)
98        throws GUIModelException, IllegalArgumentException
99    {
100        if ((guiElementPath == null) || (guiElementPath.size() <= 0)) {
101            throw new IllegalArgumentException("GUI element path must contain at least one element");
102        }
103
104        List<IGUIElementSpec> remainingPath = new LinkedList<IGUIElementSpec>();
105
106        for (IGUIElementSpec spec : guiElementPath) {
107            remainingPath.add(spec);
108        }
109
110        return integratePath(root, remainingPath, guiElementFactory);
111    }
112
113    /**
114     * <p>
115     * Returns all children of the provided GUI element or null, if it does not have any or the node
116     * is unknown.
117     * </p>
118     *
119     * @param guiElement
120     *            the GUI element of which the children shall be returned
121     *
122     * @return As described
123     */
124    public List<IGUIElement> getChildren(IGUIElement guiElement) {
125        List<IGUIElement> result = null;
126        for (TreeNode node : allNodes) {
127            if (node.guiElement.equals(guiElement)) {
128                if (result == null) {
129                    result = new ArrayList<IGUIElement>();
130
131                    if (node.children != null) {
132                        for (TreeNode child : node.children) {
133                            result.add(child.guiElement);
134                        }
135                    }
136                }
137                else {
138                    Console
139                        .traceln(Level.SEVERE,
140                                 "Multiple nodes in the internal GUI model match the same GUI element. "
141                                     + "This should not be the case and the GUI model is probably invalid.");
142                }
143            }
144        }
145
146        return result;
147    }
148
149    /**
150     * <p>
151     * Returns the parent GUI element of the provided GUI element or null, if it does not have a
152     * parent (i.e. if it is a root node) or if the node is unknown.
153     * </p>
154     *
155     * @param guiElement
156     *            the GUI element of which the parent shall be returned
157     *
158     * @return As described
159     */
160    public IGUIElement getParent(IGUIElement guiElement) {
161        IGUIElement parent = null;
162
163        for (TreeNode node : allNodes) {
164            for (TreeNode child : node.children) {
165                if (child.guiElement.equals(guiElement)) {
166                    if (parent != null) {
167                        parent = node.guiElement;
168                    }
169                    else {
170                        Console
171                            .traceln(Level.SEVERE,
172                                     "Multiple nodes in the internal GUI model match the same GUI element. "
173                                         + "This should not be the case and the GUI model is probably invalid.");
174                    }
175                }
176            }
177        }
178
179        return parent;
180    }
181
182    /**
183     * <p>
184     * Returns all root GUI elements of the model or an empty list, if the model is empty
185     * </p>
186     *
187     * @return As described
188     */
189    public List<IGUIElement> getRootElements() {
190        List<IGUIElement> roots = new ArrayList<IGUIElement>();
191
192        if (root.children != null) {
193            for (TreeNode rootChild : root.children) {
194                roots.add(rootChild.guiElement);
195            }
196        }
197
198        return roots;
199    }
200
201    /**
202     * <p>
203     * dumps the GUI model to the provided stream. Each node is represented through its toString()
204     * method. If a node has children, those are dumped indented and surrounded by braces.
205     * </p>
206     *
207     * @param out
208     *            The stream to dump the textual representation of the model to
209     * @param encoding
210     *            The encoding to be used while dumping
211     */
212    public void dump(OutputStream out, String encoding) {
213        PrintStream stream;
214
215        if (out instanceof PrintStream) {
216            stream = (PrintStream) out;
217        }
218        else {
219            String enc = encoding == null ? "UTF-8" : encoding;
220            try {
221                stream = new PrintStream(out, true, enc);
222            }
223            catch (UnsupportedEncodingException e) {
224                throw new IllegalArgumentException("encodind " + enc + " not supported");
225            }
226        }
227
228        for (IGUIElement root : getRootElements()) {
229            dumpGUIElement(stream, root, "");
230        }
231    }
232
233    /**
234     * <p>
235     * By calling this method, the GUIModel is traversed and similar nodes are merged.
236     * </p>
237     *
238     */
239    public void condenseModel() {
240        mergeSubTree(root);
241    }
242   
243    /**
244     * <p>
245     * Merges the tree nodes of two GUI elements. The GUI elements need to have the same parent.
246     * </p>
247     *
248     * @param guiElement1
249     *            the first merge GUI element
250     * @param guiElement2
251     *            the second merge GUI element
252     * @throws IllegalArgumentException
253     *             thrown if the two GUI elements do not have the same parent
254     */
255    public void mergeGUIElements(IGUIElement guiElement1, IGUIElement guiElement2)
256        throws IllegalArgumentException
257    {
258        // check if both nodes have the same parent
259        IGUIElement parentElement = guiElement1.getParent();
260        if (parentElement != null && !parentElement.equals(guiElement2.getParent())) {
261            throw new IllegalArgumentException("can only merge nodes with the same parent");
262        }
263
264        // get the TreeNode of the parent of the GUI elements
265        TreeNode parent = findNode(parentElement);
266
267        // get the TreeNodes for both GUI elements
268        TreeNode node1 = findNode(guiElement1);
269        TreeNode node2 = findNode(guiElement2);
270
271        if (node1 == null || node2 == null) {
272            throw new IllegalArgumentException(
273                                               "Error while merging nodes: one element is not part of the GUI model!");
274        }
275
276        TreeNode replacement = mergeTreeNodes(node1, node2);
277
278        if (parent != null) {
279            // remove node1 and node2 from the parent's children and add the replacement instead
280            // assumes that there are no duplicates of node1 and node2
281            if (parent.children != null) {
282                parent.children.set(parent.children.indexOf(node1), replacement);
283                parent.children.remove(node2);
284            }
285        }
286
287    }
288
289    /**
290     * <p>
291     * internally integrates a path as the children of the provided parent node. This method is
292     * recursive and calls itself, for the child of the parent node, that matches the first element
293     * in the remaining path.
294     * </p>
295     *
296     * @param parentNode
297     *            the parent node to add children for
298     * @param guiElementPath
299     *            the path of children to be created starting with the parent node
300     * @param guiElementFactory
301     *            the GUI element factory to be used for instantiating GUI element objects
302     *
303     * @return The GUI element object representing the GUI element denoted by the provided path
304     *
305     * @throws GUIModelException
306     *             thrown in cases such as the GUI element object could not be instantiated
307     */
308    private IGUIElement integratePath(TreeNode parentNode,
309                                      List<? extends IGUIElementSpec> remainingPath,
310                                      IGUIElementFactory guiElementFactory)
311        throws GUIModelException
312    {
313        IGUIElementSpec specToIntegrateElementFor = remainingPath.remove(0);
314
315        TreeNode child = findEqualChild(parentNode, specToIntegrateElementFor);
316        if (child == null) {
317            IGUIElement newElement =
318                guiElementFactory.instantiateGUIElement(specToIntegrateElementFor,
319                                                        parentNode.guiElement);
320
321            child = parentNode.addChild(newElement);
322        }
323
324        if (remainingPath.size() > 0) {
325            return integratePath(child, remainingPath, guiElementFactory);
326        }
327        else {
328            return child.guiElement;
329        }
330    }
331
332    /**
333     * <p>
334     * Searches the children of a tree node to see if the {@link IGUIElementSpec} of equals the
335     * specification of the {@link TreeNode#guiElement} of the child. If a match is found, the child
336     * is returned.
337     * </p>
338     *
339     * @param parentNode
340     *            parent node whose children are searched
341     * @param specToMatch
342     *            specification that is searched for
343     * @return matching child node or null if no child matches
344     */
345    private TreeNode findEqualChild(TreeNode parentNode, IGUIElementSpec specToMatch) {
346        if (parentNode.children != null) {
347            for (TreeNode child : parentNode.children) {
348                if (specToMatch.equals(child.guiElement.getSpecification())) {
349                    return child;
350                }
351            }
352        }
353        return null;
354    }
355
356    /**
357     * <p>
358     * Merges all similar nodes in the sub-tree of the GUI model defined by the subTreeRoot.
359     * </p>
360     * <p>
361     * The merging order is a bottom-up. This means, that we first call mergeSubTree recursively for
362     * the grand children of the subTreeRoot, before we merge subTreeRoot.
363     * </p>
364     * <p>
365     * The merging strategy is top-down. This means, that every time we merge two child nodes, we
366     * call mergeSubTree recursively for all children of the merged nodes in order to check if we
367     * can merge the children, too.
368     * </p>
369     *
370     * @param subTreeRoot
371     *            root node of the sub-tree that is merged
372     */
373    private void mergeSubTree(TreeNode subTreeRoot) {
374        if (subTreeRoot.children == null || subTreeRoot.children.isEmpty()) {
375            return;
376        }
377
378        // lets first merge the grand children of the parentNode
379        for (TreeNode child : subTreeRoot.children) {
380            mergeSubTree(child);
381        }
382
383        boolean performedMerge;
384
385        do {
386            performedMerge = false;
387            for (int i = 0; !performedMerge && i < subTreeRoot.children.size(); i++) {
388                IGUIElementSpec elemSpec1 =
389                    subTreeRoot.children.get(i).guiElement.getSpecification();
390                for (int j = i + 1; !performedMerge && j < subTreeRoot.children.size(); j++) {
391                    IGUIElementSpec elemSpec2 =
392                        subTreeRoot.children.get(j).guiElement.getSpecification();
393                    if (elemSpec1.getSimilarity(elemSpec2)) {
394                        TreeNode replacement =
395                            mergeTreeNodes(subTreeRoot.children.get(i), subTreeRoot.children.get(j));
396
397                        subTreeRoot.children.set(i, replacement);
398                        subTreeRoot.children.remove(j);
399                        performedMerge = true;
400                        i--;
401                        break;
402                    }
403                }
404            }
405        }
406        while (performedMerge);
407    }
408
409    /**
410     * <p>
411     * merges two nodes with each other. Merging means registering the GUI element objects with each
412     * other for equality checks. Further it add all children of both nodes to a new replacing node.
413     * Afterwards, all similar nodes of the replacement node are merged as well.
414     * </p>
415     *
416     * @param treeNode1
417     *            the first of the two nodes to be merged
418     * @param treeNode2
419     *            the second of the two nodes to be merged
420     * @return a tree node being the merge of the two provided nodes.
421     */
422    private TreeNode mergeTreeNodes(TreeNode treeNode1, TreeNode treeNode2) {
423        // the following two lines are needed to preserve the references to the existing GUI
424        // elements. If two elements are the same, one should be deleted to make the elements
425        // singletons again. However, there may exist references to both objects. To preserve
426        // these, we simply register the equal GUI elements with each other so that an equals
427        // check can return true.
428        treeNode1.guiElement.addEqualGUIElement(treeNode2.guiElement);
429        treeNode2.guiElement.addEqualGUIElement(treeNode1.guiElement);
430
431        // and now a replacement node that is the merge of treeNode1 and treeNode2 is created
432        TreeNode replacement = new TreeNode();
433        replacement.guiElement = treeNode1.guiElement;
434        if (treeNode1.children != null) {
435            for (TreeNode child : treeNode1.children) {
436                replacement.addChildNode(child);
437            }
438        }
439        if (treeNode2.children != null) {
440            for (TreeNode child : treeNode2.children) {
441                replacement.addChildNode(child);
442            }
443        }
444
445        mergeSubTree(replacement);
446
447        replacement.guiElement.updateSpecification(treeNode2.guiElement.getSpecification());
448
449        // finally, update the known nodes list
450        // if you don't do this getChildren will return wrong things and very bad things happen!
451        allNodes.remove(treeNode1);
452        allNodes.remove(treeNode2);
453        allNodes.add(replacement);
454
455        return replacement;
456    }
457
458    /**
459     * <p>
460     * dumps a GUI element to the stream. A dump contains the toString() representation of the GUI
461     * element as well as a indented list of its children surrounded by braces.
462     * </p>
463     *
464     * @param out
465     *            {@link PrintStream} where the guiElement is dumped to
466     * @param guiElement
467     *            the guiElement whos string represenation is dumped
468     * @param indent
469     *            indent string of the dumping
470     */
471    private void dumpGUIElement(PrintStream out, IGUIElement guiElement, String indent) {
472        out.print(indent);
473        out.print(guiElement);
474
475        List<IGUIElement> children = getChildren(guiElement);
476
477        if ((children != null) && (children.size() > 0)) {
478            out.println(" {");
479
480            for (IGUIElement child : children) {
481                dumpGUIElement(out, child, indent + "  ");
482            }
483
484            out.print(indent);
485            out.print("}");
486        }
487
488        out.println();
489    }
490   
491    /**
492     * <p>
493     * Retrieves the TreeNode associated with a GUI element. Returns null if no such TreeNode is
494     * found.
495     * </p>
496     *
497     * @param element
498     *            the GUI element
499     * @return associated TreeNode; null if no such node exists
500     */
501    private TreeNode findNode(IGUIElement element) {
502        if (element == null) {
503            return null;
504        }
505
506        TreeNode result = null;
507        for (TreeNode node : allNodes) {
508            if (node.guiElement.equals(element)) {
509                if (result == null) {
510                    result = node;
511                }
512                else {
513                    Console
514                        .traceln(Level.SEVERE,
515                                 "Multiple nodes in the internal GUI model match the same GUI element. "
516                                     + "This should not be the case and the GUI model is probably invalid.");
517                }
518            }
519        }
520        return result;
521    }
522
523    /**
524     * <p>
525     * Used internally for building up the tree of GUI elements.
526     * </p>
527     *
528     * @version 1.0
529     * @author Patrick Harms, Steffen Herbold
530     */
531    private class TreeNode {
532
533        /**
534         * <p>
535         * GUI element associated with the TreeNode.
536         * </p>
537         */
538        private IGUIElement guiElement;
539
540        /**
541         * <p>
542         * Children of the TreeNode.
543         * </p>
544         */
545        private List<TreeNode> children;
546
547        /**
548         * <p>
549         * Adds a child to the current node while keeping all lists of nodes up to date
550         * </p>
551         *
552         * @param guiElement
553         *            GUI element that will be associated with the new child
554         * @return the added child
555         */
556        private TreeNode addChild(IGUIElement guiElement) {
557            if (children == null) {
558                children = new ArrayList<TreeNode>();
559            }
560
561            TreeNode child = new TreeNode();
562            child.guiElement = guiElement;
563            children.add(child);
564
565            allNodes.add(child);
566
567            return child;
568        }
569
570        /**
571         *
572         * <p>
573         * Adds a TreeNode as child to the current node. This way, the whole sub-tree is added.
574         * </p>
575         *
576         * @param node
577         *            child node that is added
578         * @return node that has been added
579         */
580        private TreeNode addChildNode(TreeNode node) {
581            if (children == null) {
582                children = new ArrayList<TreeNode>();
583            }
584            children.add(node);
585            return node;
586        }
587
588        /*
589         * (non-Javadoc)
590         *
591         * @see java.lang.Object#toString()
592         */
593        @Override
594        public String toString() {
595            return guiElement.toString();
596        }
597    }
598}
Note: See TracBrowser for help on using the repository browser.