source: trunk/autoquest-plugin-jfc/src/main/java/de/ugoe/cs/autoquest/plugin/jfc/JFCSimplifiedLogParser.java @ 2250

Last change on this file since 2250 was 2146, checked in by pharms, 7 years ago
  • refactored GUI model so that hierarchical event target structures can also be used and created by plugins not being strictly for GUIs
  • Property svn:mime-type set to text/plain
File size: 24.6 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.plugin.jfc;
16
17import java.awt.event.MouseEvent;
18import java.io.File;
19import java.io.FileInputStream;
20import java.io.FileNotFoundException;
21import java.io.IOException;
22import java.io.InputStreamReader;
23import java.io.UnsupportedEncodingException;
24import java.util.ArrayList;
25import java.util.Collection;
26import java.util.HashMap;
27import java.util.LinkedList;
28import java.util.List;
29import java.util.Map;
30import java.util.logging.Level;
31
32import javax.xml.parsers.ParserConfigurationException;
33import javax.xml.parsers.SAXParser;
34import javax.xml.parsers.SAXParserFactory;
35
36import org.xml.sax.Attributes;
37import org.xml.sax.InputSource;
38import org.xml.sax.SAXException;
39import org.xml.sax.SAXParseException;
40import org.xml.sax.helpers.DefaultHandler;
41
42import de.ugoe.cs.autoquest.eventcore.Event;
43import de.ugoe.cs.autoquest.eventcore.EventTargetModelException;
44import de.ugoe.cs.autoquest.eventcore.gui.IInteraction;
45import de.ugoe.cs.autoquest.eventcore.gui.KeyPressed;
46import de.ugoe.cs.autoquest.eventcore.gui.KeyReleased;
47import de.ugoe.cs.autoquest.eventcore.gui.KeyboardFocusChange;
48import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonDown;
49import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonInteraction;
50import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonUp;
51import de.ugoe.cs.autoquest.eventcore.gui.MouseClick;
52import de.ugoe.cs.autoquest.eventcore.guimodel.GUIElementTree;
53import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModel;
54import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement;
55import de.ugoe.cs.autoquest.keyboardmaps.VirtualKey;
56import de.ugoe.cs.autoquest.plugin.jfc.eventcore.JFCEventId;
57import de.ugoe.cs.autoquest.plugin.jfc.guimodel.JFCGUIElement;
58import de.ugoe.cs.autoquest.plugin.jfc.guimodel.JFCGUIElementSpec;
59import de.ugoe.cs.util.console.Console;
60
61/**
62 * <p>
63 * This class provides functionality to parse XML log files generated by the JFCMonitor of
64 * autoquest. The result of parsing a file is a collection of event sequences.
65 * </p>
66 *
67 * @author Fabian Glaser
68 * @author Steffen Herbold
69 * @version 1.0
70 */
71public class JFCSimplifiedLogParser extends DefaultHandler {
72
73    /**
74     * <p>
75     * Map that holds events that had no registered target GUI element during parsing. Keys are the
76     * IDs of the unregistered targets.
77     * </p>
78     */
79    private Map<Long, List<Event>> eventsWithoutTargets;
80
81    /**
82     * <p>
83     * Collection of event sequences that is contained in the log file, which is parsed.
84     * </p>
85     */
86    private Collection<List<Event>> sequences;
87
88    /**
89     * <p>
90     * Internal handle to the id of the event that is currently being parsed.
91     * </p>
92     */
93    private JFCEventId currentEventId;
94
95    /**
96     *
97     * <p>
98     * Internal handle to the hashcode of the GUI element, that is currently parsed.
99     * </p>
100     */
101    private Long currentGUIElementHash;
102
103    /**
104     *
105     * <p>
106     * Internal handle to the hashcode of the parent of the GUI element, that is currently parsed.
107     * </p>
108     */
109    private Long currentParentHash;
110
111    /**
112     * <p>
113     * Internal handle to the source of the event that is currently being parsed.
114     * </p>
115     */
116    private Long currentEventSource;
117
118    /**
119     * <p>
120     * Internal handle to the timestamp of the event that is currently being parsed.
121     */
122    private Long currentEventTimestamp = -1l;
123
124    /**
125     * <p>
126     * Internal handle to the parameters of the event currently being parsed.
127     * </p>
128     */
129    private Map<String, String> currentEventParameters;
130
131    /**
132     * <p>
133     * Internal handle to the event sequence that is currently being parsed.
134     * </p>
135     */
136    private List<Event> currentSequence;
137
138    /**
139     * <p>
140     * internal handle to the specification currently parsed for a GUI element
141     * </p>
142     */
143    private JFCGUIElementSpec currentGuiElementSpec;
144
145    /**
146     * <p>
147     * maps the hashes of GUI elements stored in the logs to unique ids of GUI elements.
148     * This is required, as the same hash may be used for several GUI elements in the log files.
149     * </p>
150     */
151    private Map<Long, Long> hashToGuiElementIdMapping = new HashMap<>();
152
153    /**
154     * <p>
155     * counter to get unique GUI element ids
156     * </p>
157     */
158    private long nextGuiElementId = 0;
159   
160    /**
161     * <p>
162     * internal handle to the parsed GUI structure, stored in a GUIElementTree
163     * </p>
164     */
165    private GUIElementTree<Long> currentGUIElementTree;
166
167    /**
168     * <p>
169     * internal handle to the class ancestors
170     * </p>
171     */
172    private List<String> currentTypeHierarchy;
173
174    /**
175     * <p>
176     * this is used to check, if for every pressed key, there is a release of it
177     * </p>
178     */
179    private List<VirtualKey> mPressedKeys = new ArrayList<VirtualKey>();
180
181    /**
182     * <p>
183     * a specification for event ids to be omitted by the parser
184     * </p>
185     */
186    private Collection<JFCEventId> eventFilter;
187
188    /**
189     * <p>
190     * Constructor. Creates a new JFCLogParser with a default event filter. This ignores focus
191     * events, mouse pressed, and mouse released events.
192     * </p>
193     */
194    public JFCSimplifiedLogParser() {
195        sequences = new LinkedList<List<Event>>();
196        currentSequence = null;
197        eventsWithoutTargets = new HashMap<Long, List<Event>>();
198        // setupDefaultEventFilter();
199    }
200
201    /**
202     * <p>
203     * Constructor. Creates a new JFCLogParser with a specific event filter. The events in the
204     * provided collection are ignored by the parser. As events, the constants of the different
205     * event classes must be used. E.g. creating a collection and putting
206     * <code>MouseEvent.MOUSE_PRESSED</code> will cause the parser to ignore all mouse pressed
207     * events. If the provided collection is null, no event is ignored.
208     * </p>
209     *
210     * @param ignoredEvents
211     *            the events to be ignored by the parser, can be null
212     */
213    public JFCSimplifiedLogParser(Collection<JFCEventId> ignoredEvents) {
214        sequences = new LinkedList<List<Event>>();
215        currentSequence = null;
216        eventsWithoutTargets = new HashMap<Long, List<Event>>();
217        eventFilter = ignoredEvents;
218    }
219
220    /**
221     * <p>
222     * Parses a log file written by the JFCMonitor and creates a collection of event sequences.
223     * </p>
224     *
225     * @param filename
226     *            name and path of the log file
227     */
228    public void parseFile(String filename) {
229        if (filename == null) {
230            throw new IllegalArgumentException("filename must not be null");
231        }
232
233        parseFile(new File(filename));
234    }
235
236    /**
237     * <p>
238     * Parses a log file written by the JFCMonitor and creates a collection of event sequences.
239     * </p>
240     *
241     * @param file
242     *            name and path of the log file
243     */
244    public void parseFile(File file) {
245        if (file == null) {
246            throw new IllegalArgumentException("file must not be null");
247        }
248
249        SAXParserFactory spf = SAXParserFactory.newInstance();
250        spf.setValidating(true);
251
252        SAXParser saxParser = null;
253        InputSource inputSource = null;
254        try {
255            saxParser = spf.newSAXParser();
256            inputSource =
257                new InputSource(new InputStreamReader(new FileInputStream(file), "UTF-8"));
258        }
259        catch (UnsupportedEncodingException e) {
260            Console.printerr("Error parsing file + " + file.getName());
261            Console.logException(e);
262            return;
263        }
264        catch (ParserConfigurationException e) {
265            Console.printerr("Error parsing file + " + file.getName());
266            Console.logException(e);
267            return;
268        }
269        catch (SAXException e) {
270            Console.printerr("Error parsing file + " + file.getName());
271            Console.logException(e);
272            return;
273        }
274        catch (FileNotFoundException e) {
275            Console.printerr("Error parsing file + " + file.getName());
276            Console.logException(e);
277            return;
278        }
279        if (inputSource != null) {
280            inputSource.setSystemId("file://" + file.getAbsolutePath());
281            try {
282                if (saxParser == null) {
283                    throw new RuntimeException("SAXParser creation failed");
284                }
285                saxParser.parse(inputSource, this);
286            }
287            catch (SAXParseException e) {
288                Console.printerrln("Failure parsing file in line " + e.getLineNumber() +
289                    ", column " + e.getColumnNumber() + ".");
290                Console.logException(e);
291                return;
292            }
293            catch (SAXException e) {
294                Console.printerr("Error parsing file + " + file.getName());
295                Console.logException(e);
296                return;
297            }
298            catch (IOException e) {
299                Console.printerr("Error parsing file + " + file.getName());
300                Console.logException(e);
301                return;
302            }
303        }
304        if (!eventsWithoutTargets.isEmpty()) {
305            Console.printerr("Some events reference GUI elements that are not part of logfile. " +
306                        "These events have been parsed without target.");
307        }
308    }
309
310    /**
311     * <p>
312     * Returns the collection of event sequences that is obtained from parsing log files.
313     * </p>
314     *
315     * @return collection of event sequences
316     */
317    public Collection<List<Event>> getSequences() {
318        return sequences;
319    }
320
321    /**
322     * <p>
323     * Returns the GUI model that is obtained from parsing log files.
324     * </p>
325     *
326     * @return GUIModel
327     */
328    public GUIModel getGuiModel() {
329        return currentGUIElementTree.getGUIModel();
330    }
331
332    /*
333     * (non-Javadoc)
334     *
335     * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String,
336     * java.lang.String, org.xml.sax.Attributes)
337     */
338    public void startElement(String uri, String localName, String qName, Attributes atts)
339        throws SAXException
340    {
341        if (qName.equals("sessions")) {
342            currentSequence = new LinkedList<Event>();
343            if (currentGUIElementTree == null) {
344                currentGUIElementTree = new GUIElementTree<Long>();
345            }
346        }
347        if (qName.equals("newsession")) {
348            Console.traceln(Level.FINE, "start of session");
349            if (currentSequence != null && !currentSequence.isEmpty()) {
350                // create a copy of the list just to have a correctly typed one.
351                sequences.add(currentSequence.subList(0, currentSequence.size() - 1));
352            }
353            currentSequence = new LinkedList<Event>();
354        }
355        else if (qName.equals("component")) {
356            currentGUIElementHash = Long.parseLong(atts.getValue("hash"), 16);
357            currentGuiElementSpec = new JFCGUIElementSpec();
358            currentGuiElementSpec.setElementHash((int) currentGUIElementHash.longValue());
359        }
360        else if (qName.equals("event")) {
361            JFCEventId eventId = JFCEventId.parseEventId(atts.getValue("id"));
362            if ((eventFilter == null) || (!eventFilter.contains(eventId))) {
363                currentEventId = eventId;
364                currentEventParameters = new HashMap<String, String>();
365            }
366        }
367        else if (qName.equals("componentNameChange")) {
368            Long sourceHash = Long.parseLong(atts.getValue("hash"), 16);
369            String newName = atts.getValue("newName");
370            // int titleSource = Integer.parseInt(atts.getValue("titleSource"));
371            Long guiElementId = hashToGuiElementIdMapping.get(sourceHash);
372            JFCGUIElement sourceElement = (JFCGUIElement) currentGUIElementTree.find(guiElementId);
373            JFCGUIElementSpec sourceSpec = (JFCGUIElementSpec) sourceElement.getSpecification();
374            sourceSpec.setName(newName);
375        }
376        else if (qName.equals("param")) {
377            if (currentEventId != null) {
378                if ("source".equals(atts.getValue("name"))) {
379                    currentEventSource = Long.parseLong(atts.getValue("value"), 16);
380                }
381                if ("timestamp".equals(atts.getValue("name"))) {
382                    currentEventTimestamp = Long.parseLong(atts.getValue("value"));
383                }
384                currentEventParameters.put(atts.getValue("name"), atts.getValue("value"));
385            }
386            else if (currentGUIElementHash != null) {
387                if ("title".equals(atts.getValue("name"))) {
388                    currentGuiElementSpec.setName(atts.getValue("value"));
389                }
390                else if ("class".equals(atts.getValue("name"))) {
391                    currentGuiElementSpec.setType(atts.getValue("value"));
392                }
393                else if ("icon".equals(atts.getValue("name"))) {
394                    currentGuiElementSpec.setIcon(atts.getValue("value"));
395                }
396                else if ("index".equals(atts.getValue("name"))) {
397                    currentGuiElementSpec.setIndex(Integer.parseInt(atts.getValue("value")));
398                }
399                else if ("parent".equals(atts.getValue("name"))) {
400                    currentParentHash = Long.parseLong(atts.getValue("value"), 16);
401                }
402            }
403        }
404        else if (qName.equals("ancestor")) {
405            currentTypeHierarchy.add(atts.getValue("name"));
406        }
407        else if (qName.equals("ancestors")) {
408            currentTypeHierarchy = new LinkedList<String>();
409        }
410    }
411
412    /*
413     * (non-Javadoc)
414     *
415     * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String,
416     * java.lang.String)
417     */
418    @Override
419    public void endElement(String uri, String localName, String qName) throws SAXException {
420        if (qName.equals("sessions")) {
421            if (currentSequence != null && !currentSequence.isEmpty()) {
422                sequences.add(currentSequence);
423            }
424            currentSequence = null;
425        }
426        else if (qName.equals("ancestors")) {
427            currentGuiElementSpec.setTypeHierarchy(currentTypeHierarchy);
428        }
429        else if (qName.equals("component") && currentGUIElementHash != null) {
430            Long guiElementId = nextGuiElementId++;
431            Long parentGuiElementId = hashToGuiElementIdMapping.get(currentParentHash);
432           
433            try {
434                // store a mapping of the hash to a unique id and use this id for the GUI element
435                hashToGuiElementIdMapping.put(currentGUIElementHash, guiElementId);
436                currentGUIElementTree.add(guiElementId, parentGuiElementId, currentGuiElementSpec);
437            }
438            catch (EventTargetModelException e) {
439                throw new SAXException("could not handle GUI element with hash " +
440                                       currentGUIElementHash + ": " + e.getMessage(), e);
441            }
442           
443            List<Event> unhandledEvents = eventsWithoutTargets.get(currentGUIElementHash);
444            if (unhandledEvents != null) {
445                JFCGUIElement guiElement = (JFCGUIElement) currentGUIElementTree.find(guiElementId);
446                for (Event event : unhandledEvents) {
447                    event.setTarget(guiElement);
448                    guiElement.markUsed();
449                }
450                eventsWithoutTargets.remove(currentGUIElementHash);
451            }
452           
453            currentGUIElementHash = null;
454            currentParentHash = null;
455            currentTypeHierarchy = null;
456        }
457        else if (currentEventId != null) {
458            if (qName.equals("event")) {
459               
460                IGUIElement currentGUIElement = null;
461               
462                if (hashToGuiElementIdMapping.containsKey(currentEventSource)) {
463                    long guiElementId = hashToGuiElementIdMapping.get(currentEventSource);
464                    currentGUIElement = currentGUIElementTree.find(guiElementId);
465                }
466               
467                Event event;
468                // in some rare cases the target GUI element of the event is not
469                // known yet
470                if (currentGUIElement == null) {
471                    event = new Event
472                        (instantiateInteraction(currentEventId, currentEventParameters));
473                   
474                    List<Event> eventList = eventsWithoutTargets.get(currentEventSource);
475                   
476                    if (eventList == null) {
477                        eventList = new ArrayList<Event>();
478                        eventsWithoutTargets.put(currentEventSource, eventList);
479                    }
480                   
481                    eventList.add(event);
482                }
483                else {
484                    event = new Event(instantiateInteraction(currentEventId, currentEventParameters),
485                                      currentGUIElement);
486                   
487                    JFCGUIElement currentEventTarget = (JFCGUIElement) event.getTarget();
488                    currentEventTarget.markUsed();
489                }
490               
491                event.setTimestamp(currentEventTimestamp);
492                currentSequence.add(event);
493
494                currentEventParameters = null;
495                currentEventId = null;
496                currentEventTimestamp = -1l;
497            }
498        }
499    }
500
501    /**
502     * <p>
503     * depending on the event id and the event parameters, this method instantiates the concrete
504     * interaction, that took place, i.e. the event type
505     * </p>
506     *
507     * @param eventId
508     *            the id of the event
509     * @param eventParameters
510     *            the parameters provided for the event
511     *
512     * @return as described
513     *
514     * @throws SAXException
515     *             thrown if the provided event id is unknown
516     */
517    private IInteraction instantiateInteraction(JFCEventId eventId,
518                                                Map<String, String> eventParameters)
519        throws SAXException
520    {
521        switch (eventId)
522        {
523            case FOCUS_GAINED:
524                return handleNewFocus(eventId, eventParameters);
525
526            case KEY_PRESSED:
527            case KEY_RELEASED:
528            case KEY_TYPED:
529                return handleKeyAction(eventId, eventParameters);
530
531            case MOUSE_CLICKED:
532            case MOUSE_PRESSED:
533            case MOUSE_RELEASED:
534            case MOUSE_MOVED:
535            case MOUSE_ENTERED:
536            case MOUSE_EXITED:
537            case MOUSE_DRAGGED:
538            case MOUSE_WHEEL:
539                return handleMouseAction(eventId, eventParameters);
540
541            default:
542                throw new SAXException("unhandled event id " + eventId);
543        }
544    }
545
546    /**
547     * <p>
548     * handles a mouse interaction. The method determines based on the event id and the parameters
549     * which mouse button is pressed, released or clicked.
550     * </p>
551     *
552     * @param eventId
553     *            the id of the event
554     * @param eventParameters
555     *            the parameters provided for the event
556     *
557     * @return as described
558     *
559     * @throws SAXException
560     *             thrown if the provided event id or button index is unknown
561     */
562    private IInteraction handleMouseAction(JFCEventId eventId, Map<String, String> eventParameters)
563        throws SAXException
564    {
565        MouseButtonInteraction.Button button = null;
566
567        if (eventParameters.get("Button") != null) {
568            int buttonId = Integer.parseInt(eventParameters.get("Button"));
569            if (buttonId == MouseEvent.BUTTON1) {
570                button = MouseButtonInteraction.Button.LEFT;
571            }
572            else if (buttonId == MouseEvent.BUTTON2) {
573                button = MouseButtonInteraction.Button.MIDDLE;
574            }
575            else if (buttonId == MouseEvent.BUTTON3) {
576                button = MouseButtonInteraction.Button.RIGHT;
577            }
578            else {
579                throw new SAXException("unknown mouse button index " + buttonId);
580            }
581        }
582
583        if (JFCEventId.MOUSE_CLICKED == eventId) {
584            int x = Integer.parseInt(eventParameters.get("X"));
585            int y = Integer.parseInt(eventParameters.get("Y"));
586            return new MouseClick(button, x, y);
587        }
588        else if (JFCEventId.MOUSE_PRESSED == eventId) {
589            int x = Integer.parseInt(eventParameters.get("X"));
590            int y = Integer.parseInt(eventParameters.get("Y"));
591            return new MouseButtonDown(button, x, y);
592        }
593        else if (JFCEventId.MOUSE_RELEASED == eventId) {
594            int x = Integer.parseInt(eventParameters.get("X"));
595            int y = Integer.parseInt(eventParameters.get("Y"));
596            return new MouseButtonUp(button, x, y);
597        }
598        else {
599            throw new SAXException("unknown event id " + eventId);
600        }
601    }
602
603    /**
604     * <p>
605     * handles a keyboard interaction. The method determines based on the event id and the
606     * parameters which key on the keyboard is pressed or released. It further checks, if for every
607     * released key there is also a pressed event
608     * </p>
609     *
610     * @param eventId
611     *            the id of the event
612     * @param eventParameters
613     *            the parameters provided for the event
614     *
615     * @return as described
616     *
617     * @throws SAXException
618     *             thrown if the provided event id is unknown or if there is a key release without a
619     *             preceding press of the same key
620     */
621    private IInteraction handleKeyAction(JFCEventId eventId, Map<String, String> eventParameters)
622        throws SAXException
623    {
624        // TODO handle shortcuts
625        if (JFCEventId.KEY_PRESSED == eventId) {
626            VirtualKey key = VirtualKey.parseVirtualKey(eventParameters.get("KeyCode"));
627            mPressedKeys.add(key);
628
629            return new KeyPressed(key);
630        }
631        else if (JFCEventId.KEY_RELEASED == eventId) {
632            VirtualKey key = VirtualKey.parseVirtualKey(eventParameters.get("KeyCode"));
633            if (mPressedKeys.contains(key)) {
634                mPressedKeys.remove(key);
635            }
636            else {
637                Console.traceln(Level.SEVERE,
638                                "log file has an error, as it contains a key up event on key " +
639                                    key + " for which there is no preceeding key down event");
640            }
641
642            return new KeyReleased(key);
643        }
644
645        throw new SAXException("unknown event id " + eventId);
646    }
647
648    /**
649     * <p>
650     * handles explicit keyboard focus changes.
651     * </p>
652     *
653     * @param eventId
654     *            the id of the event
655     * @param eventParameters
656     *            the parameters provided for the event
657     *
658     * @return as described
659     *
660     * @throws SAXException
661     *             thrown if the provided event id is unknown
662     */
663    private IInteraction handleNewFocus(JFCEventId eventId, Map<String, String> eventParameters)
664        throws SAXException
665    {
666        if (JFCEventId.FOCUS_GAINED == eventId) {
667            return new KeyboardFocusChange();
668        }
669        else {
670            throw new SAXException("unknown event id " + eventId);
671        }
672    }
673
674    /**
675     * <p>
676     * creates a default event filter that ignores focus changes, mouse pressed and mouse released
677     * events.
678     * </p>
679     */
680    /*
681     * private void setupDefaultEventFilter() { eventFilter = new HashSet<JFCEventId>();
682     * eventFilter.add(JFCEventId.MOUSE_PRESSED); eventFilter.add(JFCEventId.MOUSE_RELEASED);
683     * eventFilter.add(JFCEventId.FOCUS_GAINED); }
684     */
685}
Note: See TracBrowser for help on using the repository browser.