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

Last change on this file since 1088 was 1088, checked in by pharms, 11 years ago
  • added a GUI model test
  • performed some bug fixes
  • added a possibility to navigate a traverser to a specific GUI element in the model
File size: 28.0 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            if (node.children != null) {
166                for (TreeNode child : node.children) {
167                    if (child.guiElement.equals(guiElement)) {
168                        if (parent != null) {
169                            parent = node.guiElement;
170                        }
171                        else {
172                            Console
173                            .traceln(Level.SEVERE,
174                                     "Multiple nodes in the internal GUI model match the same GUI element. "
175                                             + "This should not be the case and the GUI model is probably invalid.");
176                        }
177                    }
178                }
179            }
180        }
181
182        return parent;
183    }
184
185    /**
186     * <p>
187     * Returns all root GUI elements of the model or an empty list, if the model is empty
188     * </p>
189     *
190     * @return As described
191     */
192    public List<IGUIElement> getRootElements() {
193        List<IGUIElement> roots = new ArrayList<IGUIElement>();
194
195        if (root.children != null) {
196            for (TreeNode rootChild : root.children) {
197                roots.add(rootChild.guiElement);
198            }
199        }
200
201        return roots;
202    }
203   
204    /**
205     * returns a traverser for the GUI model to have efficient access to the tree of GUI elements
206     * without having direct access.
207     *
208     * @return a traverser
209     */
210    public Traverser getTraverser() {
211        return new Traverser();
212    }
213
214    /**
215     * returns a traverser for the GUI model starting at the given GUI element. Returns null, if
216     * the GUI element is not part of the model.
217     *
218     * @return a traverser
219     */
220    public Traverser getTraverser(IGUIElement startingAt) {
221        TreeNode node = findNode(startingAt);
222       
223        if (node != null) {
224            Traverser traverser = new Traverser();
225            traverser.navigateTo(node);
226            return traverser;
227        }
228        else {
229            return null;
230        }
231    }
232
233    /**
234     * <p>
235     * dumps the GUI model to the provided stream. Each node is represented through its toString()
236     * method. If a node has children, those are dumped indented and surrounded by braces.
237     * </p>
238     *
239     * @param out
240     *            The stream to dump the textual representation of the model to
241     * @param encoding
242     *            The encoding to be used while dumping
243     */
244    public void dump(OutputStream out, String encoding) {
245        PrintStream stream;
246
247        if (out instanceof PrintStream) {
248            stream = (PrintStream) out;
249        }
250        else {
251            String enc = encoding == null ? "UTF-8" : encoding;
252            try {
253                stream = new PrintStream(out, true, enc);
254            }
255            catch (UnsupportedEncodingException e) {
256                throw new IllegalArgumentException("encodind " + enc + " not supported");
257            }
258        }
259
260        for (TreeNode node : root.children) {
261            dumpGUIElement(stream, node, "");
262        }
263    }
264
265    /**
266     * <p>
267     * By calling this method, the GUIModel is traversed and similar nodes are merged.
268     * </p>
269     *
270     */
271    public void condenseModel() {
272        mergeSubTree(root);
273    }
274   
275    /**
276     * <p>
277     * Merges the tree nodes of two GUI elements. The GUI elements need to have the same parent.
278     * </p>
279     *
280     * @param guiElement1
281     *            the first merge GUI element
282     * @param guiElement2
283     *            the second merge GUI element
284     * @throws IllegalArgumentException
285     *             thrown if the two GUI elements do not have the same parent
286     */
287    public void mergeGUIElements(IGUIElement guiElement1, IGUIElement guiElement2)
288        throws IllegalArgumentException
289    {
290        // check if both nodes have the same parent
291        IGUIElement parentElement = guiElement1.getParent();
292        if (parentElement != null && !parentElement.equals(guiElement2.getParent())) {
293            throw new IllegalArgumentException("can only merge nodes with the same parent");
294        }
295
296        // get the TreeNode of the parent of the GUI elements
297        TreeNode parent = findNode(parentElement);
298
299        // get the TreeNodes for both GUI elements
300        TreeNode node1 = findNode(guiElement1);
301        TreeNode node2 = findNode(guiElement2);
302
303        if (node1 == null || node2 == null) {
304            throw new IllegalArgumentException(
305                                               "Error while merging nodes: one element is not part of the GUI model!");
306        }
307
308        TreeNode replacement = mergeTreeNodes(node1, node2);
309
310        if (parent != null) {
311            // remove node1 and node2 from the parent's children and add the replacement instead
312            // assumes that there are no duplicates of node1 and node2
313            if (parent.children != null) {
314                parent.children.set(parent.children.indexOf(node1), replacement);
315                parent.children.remove(node2);
316            }
317        }
318
319    }
320
321    /**
322     * <p>
323     * internally integrates a path as the children of the provided parent node. This method is
324     * recursive and calls itself, for the child of the parent node, that matches the first element
325     * in the remaining path.
326     * </p>
327     *
328     * @param parentNode
329     *            the parent node to add children for
330     * @param guiElementPath
331     *            the path of children to be created starting with the parent node
332     * @param guiElementFactory
333     *            the GUI element factory to be used for instantiating GUI element objects
334     *
335     * @return The GUI element object representing the GUI element denoted by the provided path
336     *
337     * @throws GUIModelException
338     *             thrown in cases such as the GUI element object could not be instantiated
339     */
340    private IGUIElement integratePath(TreeNode parentNode,
341                                      List<? extends IGUIElementSpec> remainingPath,
342                                      IGUIElementFactory guiElementFactory)
343        throws GUIModelException
344    {
345        IGUIElementSpec specToIntegrateElementFor = remainingPath.remove(0);
346
347        TreeNode child = findEqualChild(parentNode, specToIntegrateElementFor);
348        if (child == null) {
349            IGUIElement newElement =
350                guiElementFactory.instantiateGUIElement(specToIntegrateElementFor,
351                                                        parentNode.guiElement);
352
353            child = parentNode.addChild(newElement);
354        }
355
356        if (remainingPath.size() > 0) {
357            return integratePath(child, remainingPath, guiElementFactory);
358        }
359        else {
360            return child.guiElement;
361        }
362    }
363
364    /**
365     * <p>
366     * Searches the children of a tree node to see if the {@link IGUIElementSpec} of equals the
367     * specification of the {@link TreeNode#guiElement} of the child. If a match is found, the child
368     * is returned.
369     * </p>
370     *
371     * @param parentNode
372     *            parent node whose children are searched
373     * @param specToMatch
374     *            specification that is searched for
375     * @return matching child node or null if no child matches
376     */
377    private TreeNode findEqualChild(TreeNode parentNode, IGUIElementSpec specToMatch) {
378        if (parentNode.children != null) {
379            for (TreeNode child : parentNode.children) {
380                if (specToMatch.equals(child.guiElement.getSpecification())) {
381                    return child;
382                }
383            }
384        }
385        return null;
386    }
387
388    /**
389     * <p>
390     * Merges all similar nodes in the sub-tree of the GUI model defined by the subTreeRoot.
391     * </p>
392     * <p>
393     * The merging order is a bottom-up. This means, that we first call mergeSubTree recursively for
394     * the grand children of the subTreeRoot, before we merge subTreeRoot.
395     * </p>
396     * <p>
397     * The merging strategy is top-down. This means, that every time we merge two child nodes, we
398     * call mergeSubTree recursively for all children of the merged nodes in order to check if we
399     * can merge the children, too.
400     * </p>
401     *
402     * @param subTreeRoot
403     *            root node of the sub-tree that is merged
404     */
405    private void mergeSubTree(TreeNode subTreeRoot) {
406        if (subTreeRoot.children == null || subTreeRoot.children.isEmpty()) {
407            return;
408        }
409
410        // lets first merge the grand children of the parentNode
411        for (TreeNode child : subTreeRoot.children) {
412            mergeSubTree(child);
413        }
414
415        boolean performedMerge;
416
417        do {
418            performedMerge = false;
419            for (int i = 0; !performedMerge && i < subTreeRoot.children.size(); i++) {
420                IGUIElementSpec elemSpec1 =
421                    subTreeRoot.children.get(i).guiElement.getSpecification();
422                for (int j = i + 1; !performedMerge && j < subTreeRoot.children.size(); j++) {
423                    IGUIElementSpec elemSpec2 =
424                        subTreeRoot.children.get(j).guiElement.getSpecification();
425                    if (elemSpec1.getSimilarity(elemSpec2)) {
426                        TreeNode replacement =
427                            mergeTreeNodes(subTreeRoot.children.get(i), subTreeRoot.children.get(j));
428
429                        subTreeRoot.children.set(i, replacement);
430                        subTreeRoot.children.remove(j);
431                        performedMerge = true;
432                        i--;
433                        break;
434                    }
435                }
436            }
437        }
438        while (performedMerge);
439    }
440
441    /**
442     * <p>
443     * merges two nodes with each other. Merging means registering the GUI element objects with each
444     * other for equality checks. Further it add all children of both nodes to a new replacing node.
445     * Afterwards, all similar nodes of the replacement node are merged as well.
446     * </p>
447     *
448     * @param treeNode1
449     *            the first of the two nodes to be merged
450     * @param treeNode2
451     *            the second of the two nodes to be merged
452     * @return a tree node being the merge of the two provided nodes.
453     */
454    private TreeNode mergeTreeNodes(TreeNode treeNode1, TreeNode treeNode2) {
455        // the following two lines are needed to preserve the references to the existing GUI
456        // elements. If two elements are the same, one should be deleted to make the elements
457        // singletons again. However, there may exist references to both objects. To preserve
458        // these, we simply register the equal GUI elements with each other so that an equals
459        // check can return true.
460        treeNode1.guiElement.addEqualGUIElement(treeNode2.guiElement);
461        treeNode2.guiElement.addEqualGUIElement(treeNode1.guiElement);
462
463        // and now a replacement node that is the merge of treeNode1 and treeNode2 is created
464        TreeNode replacement = new TreeNode();
465        replacement.guiElement = treeNode1.guiElement;
466        if (treeNode1.children != null) {
467            for (TreeNode child : treeNode1.children) {
468                replacement.addChildNode(child);
469            }
470        }
471        if (treeNode2.children != null) {
472            for (TreeNode child : treeNode2.children) {
473                replacement.addChildNode(child);
474            }
475        }
476
477        mergeSubTree(replacement);
478
479        replacement.guiElement.updateSpecification(treeNode2.guiElement.getSpecification());
480
481        // finally, update the known nodes list
482        // if you don't do this getChildren will return wrong things and very bad things happen!
483        allNodes.remove(treeNode1);
484        allNodes.remove(treeNode2);
485        allNodes.add(replacement);
486
487        return replacement;
488    }
489
490    /**
491     * <p>
492     * dumps a GUI element to the stream. A dump contains the toString() representation of the GUI
493     * element as well as a indented list of its children surrounded by braces. Therefore, not the
494     * GUI element itself but its tree node is provided to have an efficient access to its children
495     * </p>
496     *
497     * @param out
498     *            {@link PrintStream} where the guiElement is dumped to
499     * @param node
500     *            the guiElement's tree node of which the string representation is dumped
501     * @param indent
502     *            indent string of the dumping
503     */
504    private void dumpGUIElement(PrintStream out, TreeNode node, String indent) {
505        out.print(indent);
506        out.print(node.guiElement);
507
508        if ((node.children != null) && (node.children.size() > 0)) {
509            out.println(" {");
510
511            for (TreeNode child : node.children) {
512                dumpGUIElement(out, child, indent + "  ");
513            }
514
515            out.print(indent);
516            out.print("}");
517        }
518
519        out.println();
520    }
521   
522    /**
523     * <p>
524     * Retrieves the TreeNode associated with a GUI element. Returns null if no such TreeNode is
525     * found.
526     * </p>
527     *
528     * @param element
529     *            the GUI element
530     * @return associated TreeNode; null if no such node exists
531     */
532    private TreeNode findNode(IGUIElement element) {
533        if (element == null) {
534            return null;
535        }
536
537        TreeNode result = null;
538        for (TreeNode node : allNodes) {
539            if (node.guiElement.equals(element)) {
540                if (result == null) {
541                    result = node;
542                }
543                else {
544                    Console
545                        .traceln(Level.SEVERE,
546                                 "Multiple nodes in the internal GUI model match the same GUI element. "
547                                     + "This should not be the case and the GUI model is probably invalid.");
548                }
549            }
550        }
551        return result;
552    }
553
554    /**
555     * <p>
556     * Used externally for tree traversal without providing direct access to the tree nodes
557     * </p>
558     *
559     * @version 1.0
560     * @author Patrick Harms, Steffen Herbold
561     */
562    public class Traverser {
563       
564        /**
565         * <p>
566         * the stack of nodes on which the traverser currently works
567         * </p>
568         */
569        private Stack<StackEntry> nodeStack = new Stack<StackEntry>();
570       
571        /**
572         * <p>
573         * initializes the traverser by adding the root node of the GUI model to the stack
574         * </p>
575         */
576        private Traverser() {
577            nodeStack.push(new StackEntry(root, 0));
578        }
579       
580        /**
581         * <p>
582         * returns the first child of the current GUI element. On the first call of this method on
583         * the traverser the first of the root GUI elements of the GUI model is returned. If the
584         * current GUI element does not have children, the method returns null. If the GUI model
585         * is empty, then a call to this method will return null. The returned GUI element is the
586         * next one the traverser points to.
587         * </p>
588         *
589         * @return as described.
590         */
591        public IGUIElement firstChild() {
592            return pushChild(0);
593        }
594       
595        /**
596         * <p>
597         * returns true, if the current GUI element has a first child, i.e. if the next call to the
598         * method {@link #firstChild()} would return a GUI element or null.
599         * </p>
600         *
601         * @return as described
602         */
603        public boolean hasFirstChild() {
604            return
605                (nodeStack.peek().treeNode.children != null) &&
606                (nodeStack.peek().treeNode.children.size() > 0);
607        }
608       
609        /**
610         * <p>
611         * returns the next sibling of the current GUI element. If there is no further sibling,
612         * null is returned. If the current GUI element is one of the root nodes, the next root
613         * node of the GUI model is returned. The returned GUI element is the next one the
614         * traverser points to.
615         * </p>
616         *
617         * @return as described
618         */
619        public IGUIElement nextSibling() {
620            int lastIndex = nodeStack.pop().index;
621           
622            IGUIElement retval = pushChild(lastIndex + 1);
623            if (retval == null) {
624                pushChild(lastIndex);
625            }
626           
627            return retval;
628        }
629       
630        /**
631         * <p>
632         * returns true, if the current GUI element has a further sibling, i.e. if a call to the
633         * method {@link #nextSibling()} will return a GUI element;
634         * </p>
635         *
636         * @return as described
637         */
638        public boolean hasNextSibling() {
639            boolean result = false;
640            if (nodeStack.size() > 1) {
641                StackEntry entry = nodeStack.pop();
642                result = nodeStack.peek().treeNode.children.size() > (entry.index + 1);
643                pushChild(entry.index);
644            }
645           
646            return result;
647        }
648       
649        /**
650         * <p>
651         * returns the parent GUI element of the current GUI element. If the current GUI element
652         * is a root node, null is returned. If there is no current GUI element yet as the method
653         * {@link #firstChild()} was not called yet, null is returned.
654         * </p>
655         *
656         * @return as described
657         */
658        public IGUIElement parent() {
659            IGUIElement retval = null;
660           
661            if (nodeStack.size() > 1) {
662                nodeStack.pop();
663                retval = nodeStack.peek().treeNode.guiElement;
664            }
665           
666            return retval;
667        }
668       
669        /**
670         * <p>
671         * internal method used for changing the state of the traverser. I.e. to switch to a
672         * specific child GUI element of the current one.
673         * </p>
674         */
675        private IGUIElement pushChild(int index) {
676            IGUIElement retVal = null;
677           
678            if ((nodeStack.peek().treeNode.children != null) &&
679                (nodeStack.peek().treeNode.children.size() > index))
680            {
681                nodeStack.push
682                    (new StackEntry(nodeStack.peek().treeNode.children.get(index), index));
683                retVal = nodeStack.peek().treeNode.guiElement;
684            }
685           
686            return retVal;
687        }
688       
689        /**
690         * <p>
691         * navigates the traverser to the given node in the GUI model
692         * </p>
693         */
694        private boolean navigateTo(TreeNode node) {
695            if (hasFirstChild()) {
696                IGUIElement childElement = firstChild();
697           
698                while (childElement != null) {
699                    if (childElement.equals(node.guiElement)) {
700                        return true;
701                    }
702                    else if (navigateTo(node)) {
703                        return true;
704                    }
705                    else {
706                        childElement = nextSibling();
707                    }
708                }
709           
710                parent();
711            }
712           
713            return false;
714        }
715
716        /**
717         * <p>
718         * internal class needed to fill the stack with nodes of the GUI models and their
719         * respective index in the children of the parent node.
720         * </p>
721         */
722        private class StackEntry {
723           
724            /** */
725            private TreeNode treeNode;
726           
727            /** */
728            private int index;
729           
730            /**
731             * <p>
732             * creates a new stack entry.
733             * </p>
734             */
735            private StackEntry(TreeNode treeNode, int index) {
736                this.treeNode = treeNode;
737                this.index = index;
738            }
739        }
740    }
741
742    /**
743     * <p>
744     * Used internally for building up the tree of GUI elements.
745     * </p>
746     *
747     * @version 1.0
748     * @author Patrick Harms, Steffen Herbold
749     */
750    private class TreeNode {
751
752        /**
753         * <p>
754         * GUI element associated with the TreeNode.
755         * </p>
756         */
757        private IGUIElement guiElement;
758
759        /**
760         * <p>
761         * Children of the TreeNode.
762         * </p>
763         */
764        private List<TreeNode> children;
765
766        /**
767         * <p>
768         * Adds a child to the current node while keeping all lists of nodes up to date
769         * </p>
770         *
771         * @param guiElement
772         *            GUI element that will be associated with the new child
773         * @return the added child
774         */
775        private TreeNode addChild(IGUIElement guiElement) {
776            if (children == null) {
777                children = new ArrayList<TreeNode>();
778            }
779
780            TreeNode child = new TreeNode();
781            child.guiElement = guiElement;
782            children.add(child);
783
784            allNodes.add(child);
785
786            return child;
787        }
788
789        /**
790         *
791         * <p>
792         * Adds a TreeNode as child to the current node. This way, the whole sub-tree is added.
793         * </p>
794         *
795         * @param node
796         *            child node that is added
797         * @return node that has been added
798         */
799        private TreeNode addChildNode(TreeNode node) {
800            if (children == null) {
801                children = new ArrayList<TreeNode>();
802            }
803            children.add(node);
804            return node;
805        }
806
807        /*
808         * (non-Javadoc)
809         *
810         * @see java.lang.Object#toString()
811         */
812        @Override
813        public String toString() {
814            return guiElement.toString();
815        }
816    }
817}
Note: See TracBrowser for help on using the repository browser.