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

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