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

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