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

Last change on this file since 783 was 783, checked in by sherbold, 12 years ago
  • improved exception handling of the JFCLogParser
File size: 24.5 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                e.printStackTrace();
259            }
260            catch (SAXException e) {
261                Console.printerr("Error parsing file + " + file.getName());
262                Console.logException(e);
263                return;
264            }
265            catch (IOException e) {
266                Console.printerr("Error parsing file + " + file.getName());
267                Console.logException(e);
268                return;
269            }
270        }
271    }
272
273    /**
274     * <p>
275     * Returns the collection of event sequences that is obtained from parsing log files.
276     * </p>
277     *
278     * @return collection of event sequences
279     */
280    public Collection<List<Event>> getSequences() {
281        return sequences;
282    }
283
284    /**
285     * <p>
286     * Returns the gui model that is obtained from parsing log files.
287     * </p>
288     *
289     * @return collection of event sequences
290     */
291    public GUIModel getGuiModel() {
292        return guiModel;
293    }
294
295    /*
296     * (non-Javadoc)
297     *
298     * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String,
299     * java.lang.String, org.xml.sax.Attributes)
300     */
301    public void startElement(String uri, String localName, String qName, Attributes atts)
302        throws SAXException
303    {
304        if (qName.equals("sessions")) {
305            currentSequence = new LinkedList<Event>();
306        }
307        if (qName.equals("newsession")) {
308            Console.traceln(Level.FINE, "start of session");
309            if (currentSequence != null && !currentSequence.isEmpty()) {
310                // create a copy of the list just to have a correctly typed one.
311                sequences.add(currentSequence.subList(0, currentSequence.size() - 1));
312            }
313            currentSequence = new LinkedList<Event>();
314        }
315        else if (qName.equals("event")) {
316            JFCEventId eventId = JFCEventId.parseEventId(atts.getValue("id"));
317            if ((eventFilter == null) || (!eventFilter.contains(eventId))) {
318                currentEventId = eventId;
319                currentEventParameters = new HashMap<String, String>();
320                currentSourceParameters = new HashMap<String, String>();
321                paramSource = ParamSource.EVENT;
322            }
323        }
324        else if (currentEventId != null) {
325            if (qName.equals("param")) {
326                if (paramSource == ParamSource.EVENT) {
327                    currentEventParameters.put(atts.getValue("name"), atts.getValue("value"));
328                }
329                else if (paramSource == ParamSource.SOURCE) {
330                    currentSourceParameters.put(atts.getValue("name"), atts.getValue("value"));
331                }
332                else if (paramSource == ParamSource.COMPONENT) {
333                    if ("title".equals(atts.getValue("name"))) {
334                        currentGuiElementSpec.setName(atts.getValue("value"));
335                    }
336                    else if ("class".equals(atts.getValue("name"))) {
337                        currentGuiElementSpec.setType(atts.getValue("value"));
338                    }
339                    else if ("icon".equals(atts.getValue("name"))) {
340                        currentGuiElementSpec.setIcon(atts.getValue("value"));
341                    }
342                    else if ("index".equals(atts.getValue("name"))) {
343                        currentGuiElementSpec.setIndex(Integer.parseInt(atts.getValue("value")));
344                    }
345                    else if ("hash".equals(atts.getValue("name"))) {
346                        currentGuiElementSpec.setElementHash
347                            ((int) Long.parseLong(atts.getValue("value"), 16));
348                    }
349                }
350            }
351            else if (qName.equals("source")) {
352                paramSource = ParamSource.SOURCE;
353            }
354            else if (qName.equals("parent")) {
355                paramSource = ParamSource.PARENT;
356            }
357            else if (qName.equals("component")) {
358                paramSource = ParamSource.COMPONENT;
359                currentGuiElementSpec = new JFCGUIElementSpec();
360            }
361        }
362    }
363
364    /*
365     * (non-Javadoc)
366     *
367     * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String,
368     * java.lang.String)
369     */
370    @Override
371    public void endElement(String uri, String localName, String qName) throws SAXException {
372        if (qName.equals("sessions")) {
373            if (currentSequence != null && !currentSequence.isEmpty()) {
374                sequences.add(currentSequence);
375            }
376            currentSequence = null;
377        }
378        else if (currentEventId != null) {
379            if (qName.equals("event")) {
380               
381                if (currentGuiElementPath.size() <= 0)
382                {
383                    // no component specification available. Try to parse the GUI element from
384                    // the toString parameter
385                    currentGuiElementSpec = new JFCGUIElementSpec();
386                    getGUIElementSpecFromToString(currentSourceParameters.get("toString"));
387                    currentGuiElementPath.add(currentGuiElementSpec);
388                    currentGuiElementSpec = null;
389                }
390               
391                IGUIElement currentGUIElement;
392                try {
393                    currentGUIElement = guiModel.integratePath
394                        (currentGuiElementPath, GUIElementFactory.getInstance());
395                }
396                catch (GUIModelException e) {
397                    throw new SAXException("error in the GUI model provided in the log", e);
398                }
399               
400                Event event = new Event
401                    (instantiateInteraction(currentEventId, currentEventParameters),
402                     (currentGUIElement == null ? lastGUIElement : currentGUIElement));
403               
404                currentSequence.add(event);
405               
406                currentEventParameters = null;
407                currentSourceParameters = null;
408                currentGuiElementSpec = null;
409                currentGuiElementPath.clear();
410               
411                currentEventId = null;
412               
413                if (currentGUIElement != null) {
414                    lastGUIElement = currentGUIElement;
415                }
416               
417                currentGUIElement = null;
418            }
419            else if (qName.equals("source")) {
420                paramSource = ParamSource.EVENT;
421            }
422            else if (qName.equals("parent")) {
423                paramSource = ParamSource.SOURCE;
424            }
425            else if (qName.equals("component")) {
426                paramSource.equals(ParamSource.SOURCE);
427                currentGuiElementPath.add(currentGuiElementSpec);
428                currentGuiElementSpec = null;
429            }
430        }
431    }
432
433    /**
434     * <p>
435     * depending on the event id and the event parameters, this method instantiates the concrete
436     * interaction, that took place, i.e. the event type
437     * </p>
438     *
439     * @param eventId
440     *            the id of the event
441     * @param eventParameters
442     *            the parameters provided for the event
443     *           
444     * @return as described
445     *
446     * @throws SAXException thrown if the provided event id is unknown
447     */
448    private IInteraction instantiateInteraction(JFCEventId          eventId,
449                                                Map<String, String> eventParameters)
450      throws SAXException
451    {
452        switch (eventId)
453        {
454            case FOCUS_GAINED:
455                return handleNewFocus(eventId, eventParameters);
456
457            case KEY_PRESSED:
458            case KEY_RELEASED:
459            case KEY_TYPED:
460                return handleKeyAction(eventId, eventParameters);
461
462            case MOUSE_CLICKED:
463            case MOUSE_PRESSED:
464            case MOUSE_RELEASED:
465            case MOUSE_MOVED:
466            case MOUSE_ENTERED:
467            case MOUSE_EXITED:
468            case MOUSE_DRAGGED:
469            case MOUSE_WHEEL:
470                return handleMouseAction(eventId, eventParameters);
471
472            default:
473                throw new SAXException("unhandled event id " + eventId);
474        }
475    }
476
477    /**
478     * <p>
479     * handles a mouse interaction. The method determines based on the event id and the parameters
480     * which mouse button is pressed, released or clicked.
481     * </p>
482     *
483     * @param eventId
484     *            the id of the event
485     * @param eventParameters
486     *            the parameters provided for the event
487     *           
488     * @return as described
489     *
490     * @throws SAXException thrown if the provided event id or button index is unknown
491     */
492    private IInteraction handleMouseAction(JFCEventId eventId, Map<String, String> eventParameters)
493        throws SAXException
494    {
495        MouseButtonInteraction.Button button = null;
496
497        if (eventParameters.get("Button") != null)
498        {
499            int buttonId = Integer.parseInt(eventParameters.get("Button"));
500            if (buttonId == MouseEvent.BUTTON1)
501            {
502                button = MouseButtonInteraction.Button.LEFT;
503            }
504            else if (buttonId == MouseEvent.BUTTON2)
505            {
506                button = MouseButtonInteraction.Button.MIDDLE;
507            }
508            else if (buttonId == MouseEvent.BUTTON3)
509            {
510                button = MouseButtonInteraction.Button.RIGHT;
511            }
512            else
513            {
514                throw new SAXException("unknown mouse button index " + buttonId);
515            }
516        }
517
518        if (JFCEventId.MOUSE_CLICKED == eventId)
519        {
520            return new MouseClick(button);
521        }
522        else if (JFCEventId.MOUSE_PRESSED == eventId)
523        {
524            return new MouseButtonDown(button);
525        }
526        else if (JFCEventId.MOUSE_RELEASED == eventId)
527        {
528            return new MouseButtonUp(button);
529        }
530        else
531        {
532            throw new SAXException("unknown event id " + eventId);
533        }
534    }
535
536    /**
537     * <p>
538     * handles a keyboard interaction. The method determines based on the event id and the
539     * parameters which key on the keyboard is pressed or released. It further checks, if for
540     * every released key there is also a pressed event
541     * </p>
542     *
543     * @param eventId
544     *            the id of the event
545     * @param eventParameters
546     *            the parameters provided for the event
547     *           
548     * @return as described
549     *
550     * @throws SAXException thrown if the provided event id is unknown or if there is a key
551     *                      release without a preceding press of the same key
552     */
553    private IInteraction handleKeyAction(JFCEventId eventId, Map<String, String> eventParameters)
554      throws SAXException
555    {
556        // TODO handle shortcuts
557        if (JFCEventId.KEY_PRESSED == eventId)
558        {
559            VirtualKey key = VirtualKey.parseVirtualKey(eventParameters.get("KeyCode"));
560            mPressedKeys.add(key);
561
562            return new KeyPressed(key);
563        }
564        else if (JFCEventId.KEY_RELEASED == eventId)
565        {
566            VirtualKey key = VirtualKey.parseVirtualKey(eventParameters.get("KeyCode"));
567            if (mPressedKeys.contains(key))
568            {
569                mPressedKeys.remove(key);
570            }
571            else
572            {
573                Console.printerrln("log file has an error, as it contains a key up event on key " +
574                                   key + " for which there is no preceeding key down event");
575            }
576
577            return new KeyReleased(key);
578        }
579
580        throw new SAXException("unknown event id " + eventId);
581    }
582
583    /**
584     * <p>
585     * handles explicit keyboard focus changes.
586     * </p>
587     *
588     * @param eventId
589     *            the id of the event
590     * @param eventParameters
591     *            the parameters provided for the event
592     *           
593     * @return as described
594     *
595     * @throws SAXException thrown if the provided event id is unknown
596     */
597    private IInteraction handleNewFocus(JFCEventId eventId, Map<String, String> eventParameters)
598        throws SAXException
599    {
600        if (JFCEventId.FOCUS_GAINED == eventId)
601        {
602            return new KeyboardFocusChange();
603        }
604        else
605        {
606            throw new SAXException("unknown event id " + eventId);
607        }
608    }
609
610    /**
611     * <p>
612     * for some events in the log file, no component specification is provided. In this case the
613     * GUI element on which the event is executed must be determined based on the
614     * <code>toString</code> parameter of the event. This is achieved through this method. The
615     * <code>toString</code> parameter does not always carry sufficient information for the GUI
616     * elements. For example the title is not necessarily provided. Therefore some of this
617     * information is generated.
618     * </p>
619     *
620     * @param toStringValue
621     *            the <code>toString</code> parameter of the event to be parsed for the GUI element
622     *           
623     * @return the appropriate GUI Element
624     *
625     * @throws SAXException thrown if the provided value of the <code>toString</code> parameter
626     *                      can not be parsed
627     */
628    private void getGUIElementSpecFromToString(String toStringValue)
629        throws SAXException
630    {
631        try
632        {
633            // match the following: <type>[<parameters>]
634            String pattern = "([\\w$\\.]*)\\[(.*)\\]";
635            Matcher matcher = Pattern.compile(pattern).matcher(toStringValue);
636
637            if (!matcher.find())
638            {
639                throw new IllegalArgumentException
640                    ("could not parse target from toString parameter");
641            }
642
643            String type = matcher.group(1);
644           
645            // match the following: <parameter>|,
646            // where <parameter> := <digitValue>|<value>|<key>"="<value>
647            pattern = "([\\w$@=\\.]*)|,";
648
649            matcher = Pattern.compile(pattern).matcher(matcher.group(2));
650           
651            float elementHash = -1;
652           
653            pattern = "(([\\d]*)|([\\w$]*)|(([\\w$@\\.]*)=([\\w$@\\.]*)))\\z";
654            Pattern valuePattern = Pattern.compile(pattern);
655           
656            while (matcher.find()) {
657                Matcher valueMatcher = valuePattern.matcher(matcher.group(1));
658                if (valueMatcher.find()) {
659                    if ((valueMatcher.group(2) != null) && (!"".equals(valueMatcher.group(2)))) {
660                        // found digit value. Those in combination usually denote the position
661                        // of the GUI element. So calculate an element has out of them
662                        elementHash += Integer.parseInt(valueMatcher.group(2));
663                    }
664                    else if ((valueMatcher.group(5) != null) &&
665                             (!"".equals(valueMatcher.group(5))) &&
666                             (valueMatcher.group(6) != null) &&
667                             (!"".equals(valueMatcher.group(6))))
668                    {
669                        // found a key value pair. Get some of them and integrate them into the hash
670                        String key = valueMatcher.group(5);
671                       
672                        if ("alignmentX".equals(key) || "alignmentY".equals(key)) {
673                            elementHash += Float.parseFloat(valueMatcher.group(6));
674                        }
675                    }
676                }
677            }
678
679            currentGuiElementSpec.setName("unknown(" + ((int) elementHash) + ")");
680            currentGuiElementSpec.setType(type);
681            currentGuiElementSpec.setIndex(-1);
682            currentGuiElementSpec.setElementHash((int) elementHash);
683        }
684        catch (Exception e)
685        {
686            throw new SAXException("could not parse target", e);
687        }
688    }
689   
690    /**
691     * <p>
692     * creates a default event filter that ignores focus changes, mouse pressed and mouse released
693     * events.
694     * </p>
695     */
696    /*private void setupDefaultEventFilter() {
697        eventFilter = new HashSet<JFCEventId>();
698        eventFilter.add(JFCEventId.MOUSE_PRESSED);
699        eventFilter.add(JFCEventId.MOUSE_RELEASED);
700        eventFilter.add(JFCEventId.FOCUS_GAINED);
701    }*/
702
703}
Note: See TracBrowser for help on using the repository browser.