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

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