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

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