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

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