// Copyright 2012 Georg-August-Universität Göttingen, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package de.ugoe.cs.autoquest.plugin.jfc; import java.awt.event.MouseEvent; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import de.ugoe.cs.autoquest.eventcore.Event; import de.ugoe.cs.autoquest.eventcore.gui.IInteraction; import de.ugoe.cs.autoquest.eventcore.gui.KeyPressed; import de.ugoe.cs.autoquest.eventcore.gui.KeyReleased; import de.ugoe.cs.autoquest.eventcore.gui.KeyboardFocusChange; import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonDown; import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonInteraction; import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonUp; import de.ugoe.cs.autoquest.eventcore.gui.MouseClick; import de.ugoe.cs.autoquest.eventcore.guimodel.GUIElementFactory; import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModel; import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModelException; import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement; import de.ugoe.cs.autoquest.keyboardmaps.VirtualKey; import de.ugoe.cs.autoquest.plugin.jfc.eventcore.JFCEventId; import de.ugoe.cs.autoquest.plugin.jfc.guimodel.JFCGUIElementSpec; import de.ugoe.cs.util.console.Console; /** *

* This class provides functionality to parse XML log files generated by the JFCMonitor of * EventBench. The result of parsing a file is a collection of event sequences. *

* * @author Steffen Herbold * @version 1.0 */ public class JFCLogParser extends DefaultHandler { /** *

* Collection of event sequences that is contained in the log file, which is parsed. *

*/ private Collection> sequences; /** *

* Internal handle to the id of the event that is currently being parsed. *

*/ private JFCEventId currentEventId; /** *

* Internal handle to the parameters of the event that is currently being parsed. *

*/ private Map currentEventParameters; /** *

* Internal handle to the source parameters of the event that is currently being parsed. *

*/ private Map currentSourceParameters; /** *

* Internal handle to the event sequence that is currently being parsed. *

*/ private List currentSequence; /** *

* internal handle to the GUI element specification currently parsed for a GUI element *

*/ private JFCGUIElementSpec currentGuiElementSpec; /** *

* internal handle to the last parsed GUI element *

*/ private List currentGuiElementPath = new ArrayList(); /** *

* internal handle to the GUI element of the previous event to be potentially reused for the * current *

*/ private IGUIElement lastGUIElement; /** *

* the model of the GUI elements, that were found during parsing *

*/ private GUIModel guiModel = new GUIModel(); /** *

* this is used to check, if for every pressed key, there is a release of it *

*/ private List mPressedKeys = new ArrayList(); /** *

* Enumeration to differentiate if a parameter belongs to an event, a source or the parent of a * source. *

* */ private enum ParamSource { EVENT, SOURCE, PARENT, COMPONENT }; /** *

* Specifies whether the parameters that are currently being read belong the the event, the * source or the parent. *

*/ private ParamSource paramSource = null; /** *

* a specification for event ids to be omitted by the parser *

*/ private Collection eventFilter; /** *

* Constructor. Creates a new JFCLogParser with a default event filter. This ignores focus * events, mouse pressed, and mouse released events. *

*/ public JFCLogParser() { sequences = new LinkedList>(); currentSequence = null; //setupDefaultEventFilter(); } /** *

* Constructor. Creates a new JFCLogParser with a specific event filter. The events in the * provided collection are ignored by the parser. As events, the constants of the different * event classes must be used. E.g. creating a collection and putting * MouseEvent.MOUSE_PRESSED will cause the parser to ignore all mouse pressed * events. If the provided collection is null, no event is ignored. *

* * @param ignoredEvents * the events to be ignored by the parser, can be null */ public JFCLogParser(Collection ignoredEvents) { sequences = new LinkedList>(); currentSequence = null; eventFilter = ignoredEvents; } /** *

* Parses a log file written by the JFCMonitor and creates a collection of event sequences. *

* * @param filename * name and path of the log file */ public void parseFile(String filename) { if (filename == null) { throw new IllegalArgumentException("filename must not be null"); } parseFile(new File(filename)); } /** *

* Parses a log file written by the JFCMonitor and creates a collection of event sequences. *

* * @param file * name and path of the log file */ public void parseFile(File file) { if (file == null) { throw new IllegalArgumentException("file must not be null"); } SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setValidating(true); SAXParser saxParser = null; InputSource inputSource = null; try { saxParser = spf.newSAXParser(); inputSource = new InputSource(new InputStreamReader(new FileInputStream(file), "UTF-8")); } catch (UnsupportedEncodingException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return; } catch (ParserConfigurationException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return; } catch (SAXException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return; } catch (FileNotFoundException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return; } if (inputSource != null) { inputSource.setSystemId("file://" + file.getAbsolutePath()); try { if (saxParser == null) { throw new RuntimeException("SAXParser creation failed"); } saxParser.parse(inputSource, this); } catch (SAXParseException e) { Console.printerrln("Failure parsing file in line " + e.getLineNumber() + ", column " + e.getColumnNumber() + "."); Console.logException(e); return; } catch (SAXException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return; } catch (IOException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return; } } } /** *

* Returns the collection of event sequences that is obtained from parsing log files. *

* * @return collection of event sequences */ public Collection> getSequences() { return sequences; } /** *

* Returns the gui model that is obtained from parsing log files. *

* * @return collection of event sequences */ public GUIModel getGuiModel() { return guiModel; } /* * (non-Javadoc) * * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, * java.lang.String, org.xml.sax.Attributes) */ public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { if (qName.equals("sessions")) { currentSequence = new LinkedList(); } if (qName.equals("newsession")) { Console.traceln(Level.FINE, "start of session"); if (currentSequence != null && !currentSequence.isEmpty()) { // create a copy of the list just to have a correctly typed one. sequences.add(currentSequence.subList(0, currentSequence.size() - 1)); } currentSequence = new LinkedList(); } else if (qName.equals("event")) { JFCEventId eventId = JFCEventId.parseEventId(atts.getValue("id")); if ((eventFilter == null) || (!eventFilter.contains(eventId))) { currentEventId = eventId; currentEventParameters = new HashMap(); currentSourceParameters = new HashMap(); paramSource = ParamSource.EVENT; } } else if (currentEventId != null) { if (qName.equals("param")) { if (paramSource == ParamSource.EVENT) { currentEventParameters.put(atts.getValue("name"), atts.getValue("value")); } else if (paramSource == ParamSource.SOURCE) { currentSourceParameters.put(atts.getValue("name"), atts.getValue("value")); } else if (paramSource == ParamSource.COMPONENT) { if ("title".equals(atts.getValue("name"))) { currentGuiElementSpec.setName(atts.getValue("value")); } else if ("class".equals(atts.getValue("name"))) { currentGuiElementSpec.setType(atts.getValue("value")); } else if ("icon".equals(atts.getValue("name"))) { currentGuiElementSpec.setIcon(atts.getValue("value")); } else if ("index".equals(atts.getValue("name"))) { currentGuiElementSpec.setIndex(Integer.parseInt(atts.getValue("value"))); } else if ("hash".equals(atts.getValue("name"))) { currentGuiElementSpec.setElementHash ((int) Long.parseLong(atts.getValue("value"), 16)); } } } else if (qName.equals("source")) { paramSource = ParamSource.SOURCE; } else if (qName.equals("parent")) { paramSource = ParamSource.PARENT; } else if (qName.equals("component")) { paramSource = ParamSource.COMPONENT; currentGuiElementSpec = new JFCGUIElementSpec(); } } } /* * (non-Javadoc) * * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, * java.lang.String) */ @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("sessions")) { if (currentSequence != null && !currentSequence.isEmpty()) { sequences.add(currentSequence); } currentSequence = null; } else if (currentEventId != null) { if (qName.equals("event")) { if (currentGuiElementPath.size() <= 0) { // no GUI element specification available. Try to parse the GUI element from // the toString parameter currentGuiElementSpec = new JFCGUIElementSpec(); getGUIElementSpecFromToString(currentSourceParameters.get("toString")); currentGuiElementPath.add(currentGuiElementSpec); currentGuiElementSpec = null; } IGUIElement currentGUIElement; try { currentGUIElement = guiModel.integratePath (currentGuiElementPath, GUIElementFactory.getInstance()); } catch (GUIModelException e) { throw new SAXException("error in the GUI model provided in the log", e); } Event event = new Event (instantiateInteraction(currentEventId, currentEventParameters), (currentGUIElement == null ? lastGUIElement : currentGUIElement)); currentSequence.add(event); currentEventParameters = null; currentSourceParameters = null; currentGuiElementSpec = null; currentGuiElementPath.clear(); currentEventId = null; if (currentGUIElement != null) { lastGUIElement = currentGUIElement; } currentGUIElement = null; } else if (qName.equals("source")) { paramSource = ParamSource.EVENT; } else if (qName.equals("parent")) { paramSource = ParamSource.SOURCE; } else if (qName.equals("component")) { paramSource.equals(ParamSource.SOURCE); currentGuiElementPath.add(currentGuiElementSpec); currentGuiElementSpec = null; } } } /** *

* depending on the event id and the event parameters, this method instantiates the concrete * interaction, that took place, i.e. the event type *

* * @param eventId * the id of the event * @param eventParameters * the parameters provided for the event * * @return as described * * @throws SAXException thrown if the provided event id is unknown */ private IInteraction instantiateInteraction(JFCEventId eventId, Map eventParameters) throws SAXException { switch (eventId) { case FOCUS_GAINED: return handleNewFocus(eventId, eventParameters); case KEY_PRESSED: case KEY_RELEASED: case KEY_TYPED: return handleKeyAction(eventId, eventParameters); case MOUSE_CLICKED: case MOUSE_PRESSED: case MOUSE_RELEASED: case MOUSE_MOVED: case MOUSE_ENTERED: case MOUSE_EXITED: case MOUSE_DRAGGED: case MOUSE_WHEEL: return handleMouseAction(eventId, eventParameters); default: throw new SAXException("unhandled event id " + eventId); } } /** *

* handles a mouse interaction. The method determines based on the event id and the parameters * which mouse button is pressed, released or clicked. *

* * @param eventId * the id of the event * @param eventParameters * the parameters provided for the event * * @return as described * * @throws SAXException thrown if the provided event id or button index is unknown */ private IInteraction handleMouseAction(JFCEventId eventId, Map eventParameters) throws SAXException { MouseButtonInteraction.Button button = null; if (eventParameters.get("Button") != null) { int buttonId = Integer.parseInt(eventParameters.get("Button")); if (buttonId == MouseEvent.BUTTON1) { button = MouseButtonInteraction.Button.LEFT; } else if (buttonId == MouseEvent.BUTTON2) { button = MouseButtonInteraction.Button.MIDDLE; } else if (buttonId == MouseEvent.BUTTON3) { button = MouseButtonInteraction.Button.RIGHT; } else { throw new SAXException("unknown mouse button index " + buttonId); } } if (JFCEventId.MOUSE_CLICKED == eventId) { int x = Integer.parseInt(eventParameters.get("X")); int y = Integer.parseInt(eventParameters.get("Y")); return new MouseClick(button, x, y); } else if (JFCEventId.MOUSE_PRESSED == eventId) { int x = Integer.parseInt(eventParameters.get("X")); int y = Integer.parseInt(eventParameters.get("Y")); return new MouseButtonDown(button, x, y); } else if (JFCEventId.MOUSE_RELEASED == eventId) { int x = Integer.parseInt(eventParameters.get("X")); int y = Integer.parseInt(eventParameters.get("Y")); return new MouseButtonUp(button, x, y); } else { throw new SAXException("unknown event id " + eventId); } } /** *

* handles a keyboard interaction. The method determines based on the event id and the * parameters which key on the keyboard is pressed or released. It further checks, if for * every released key there is also a pressed event *

* * @param eventId * the id of the event * @param eventParameters * the parameters provided for the event * * @return as described * * @throws SAXException thrown if the provided event id is unknown or if there is a key * release without a preceding press of the same key */ private IInteraction handleKeyAction(JFCEventId eventId, Map eventParameters) throws SAXException { // TODO handle shortcuts if (JFCEventId.KEY_PRESSED == eventId) { VirtualKey key = VirtualKey.parseVirtualKey(eventParameters.get("KeyCode")); mPressedKeys.add(key); return new KeyPressed(key); } else if (JFCEventId.KEY_RELEASED == eventId) { VirtualKey key = VirtualKey.parseVirtualKey(eventParameters.get("KeyCode")); if (mPressedKeys.contains(key)) { mPressedKeys.remove(key); } else { Console.traceln(Level.SEVERE, "log file has an error, as it contains a key up event on key " + key + " for which there is no preceeding key down event"); } return new KeyReleased(key); } throw new SAXException("unknown event id " + eventId); } /** *

* handles explicit keyboard focus changes. *

* * @param eventId * the id of the event * @param eventParameters * the parameters provided for the event * * @return as described * * @throws SAXException thrown if the provided event id is unknown */ private IInteraction handleNewFocus(JFCEventId eventId, Map eventParameters) throws SAXException { if (JFCEventId.FOCUS_GAINED == eventId) { return new KeyboardFocusChange(); } else { throw new SAXException("unknown event id " + eventId); } } /** *

* for some events in the log file, no GUI element specification is provided. In this case the * GUI element on which the event is executed must be determined based on the * toString parameter of the event. This is achieved through this method. The * toString parameter does not always carry sufficient information for the GUI * elements. For example the title is not necessarily provided. Therefore some of this * information is generated. *

* * @param toStringValue * the toString parameter of the event to be parsed for the GUI element * * @return the appropriate GUI element * * @throws SAXException thrown if the provided value of the toString parameter * can not be parsed */ private void getGUIElementSpecFromToString(String toStringValue) throws SAXException { try { // match the following: [] String pattern = "([\\w$\\.]*)\\[(.*)\\]"; Matcher matcher = Pattern.compile(pattern).matcher(toStringValue); if (!matcher.find()) { throw new IllegalArgumentException ("could not parse target from toString parameter"); } String type = matcher.group(1); // match the following: |, // where := ||"=" pattern = "([\\w$@=\\.]*)|,"; matcher = Pattern.compile(pattern).matcher(matcher.group(2)); float elementHash = -1; pattern = "(([\\d]*)|([\\w$]*)|(([\\w$@\\.]*)=([\\w$@\\.]*)))\\z"; Pattern valuePattern = Pattern.compile(pattern); while (matcher.find()) { Matcher valueMatcher = valuePattern.matcher(matcher.group(1)); if (valueMatcher.find()) { if ((valueMatcher.group(2) != null) && (!"".equals(valueMatcher.group(2)))) { // found digit value. Those in combination usually denote the position // of the GUI element. So calculate an element has out of them elementHash += Integer.parseInt(valueMatcher.group(2)); } else if ((valueMatcher.group(5) != null) && (!"".equals(valueMatcher.group(5))) && (valueMatcher.group(6) != null) && (!"".equals(valueMatcher.group(6)))) { // found a key value pair. Get some of them and integrate them into the hash String key = valueMatcher.group(5); if ("alignmentX".equals(key) || "alignmentY".equals(key)) { elementHash += Float.parseFloat(valueMatcher.group(6)); } } } } currentGuiElementSpec.setName("unknown(" + ((int) elementHash) + ")"); currentGuiElementSpec.setType(type); currentGuiElementSpec.setIndex(-1); currentGuiElementSpec.setElementHash((int) elementHash); } catch (Exception e) { throw new SAXException("could not parse target", e); } } /** *

* creates a default event filter that ignores focus changes, mouse pressed and mouse released * events. *

*/ /*private void setupDefaultEventFilter() { eventFilter = new HashSet(); eventFilter.add(JFCEventId.MOUSE_PRESSED); eventFilter.add(JFCEventId.MOUSE_RELEASED); eventFilter.add(JFCEventId.FOCUS_GAINED); }*/ }