source: trunk/quest-plugin-jfc/src/main/java/de/ugoe/cs/quest/plugin/jfc/JFCLogParser.java @ 828

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