// 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.android; 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.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Level; 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.EventTargetModelException; import de.ugoe.cs.autoquest.eventcore.gui.IInteraction; import de.ugoe.cs.autoquest.eventcore.gui.TextInput; import de.ugoe.cs.autoquest.eventcore.gui.TouchSingle; import de.ugoe.cs.autoquest.eventcore.guimodel.GUIElementTree; import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModel; import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement; import de.ugoe.cs.autoquest.plugin.android.guimodel.ANDROIDGUIElement; import de.ugoe.cs.autoquest.plugin.android.guimodel.ANDROIDGUIElementSpec; import de.ugoe.cs.util.console.Console; /** *

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

* * @author Florian Unger * @version 1.0 */ public class AndroidLogParser extends DefaultHandler { /* * (non-Javadoc) * * int java.lang.Object.hashCode() is used in the Androidmonitor Long is used to internally * handle and compare the number. e.g. currentGUIElementHash != null */ /** *

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

*/ private String currentEventId = null; /** *

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

*/ private Map currentEventParameters; /** *

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

*/ private Long currentEventSource; /** *

* Internal handle to the timestamp of the event that is currently being parsed. */ private Long currentEventTimestamp = -1l; /** * *

* Internal handle to the hashcode of the GUI element, that is currently parsed. *

*/ private Long currentGUIElementHash = null; /** *

* Internal handle to the parsed GUI structure, stored in a GUIElementTree *

*/ private GUIElementTree currentGUIElementTree; /** *

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

*/ private ANDROIDGUIElementSpec currentGUIElementSpec; /** * *

* Internal handle to the hashcode of the parent of the GUI element, that is currently parsed. *

*/ private Long currentParentHash; /** *

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

*/ private List currentSequence; /** *

* internal handle to the class ancestors *

*/ private List currentTypeHierarchy; /** *

* Map that holds events that had no registered target GUI element during parsing. Keys are the * IDs of the unregistered targets. *

*/ // private Map> eventsWithoutTargets; /** *

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

*/ private Collection> sequences; /** * Show parsed elements to find out failures. */ private Boolean showSteps = false; /** *

* Constructor. Creates a new AndroidLogParser. *

*/ public AndroidLogParser() { sequences = new LinkedList>(); currentSequence = null; } // TODO create a constructor which creates a new AndroidLogParser with a // specific event filter. /** *

* 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(); // set true to validate that the file is well defined 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 { // called a second time to be sure that no error happens 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 GUIModel */ public GUIModel getGuiModel() { return currentGUIElementTree.getGUIModel(); } /* * (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 { showSteps("start element: " + qName, true); if (qName.equals("sessions")) { currentSequence = new LinkedList(); if (currentGUIElementTree == null) { currentGUIElementTree = new GUIElementTree(); } } if (qName.equals("component")) { currentGUIElementHash = Long.parseLong(atts.getValue("hash")); currentGUIElementSpec = new ANDROIDGUIElementSpec(); currentGUIElementSpec.setElementHash((int) currentGUIElementHash.longValue()); } else if (qName.equals("event")) { currentEventId = atts.getValue("id"); currentEventParameters = new HashMap(); } else if (qName.equals("param")) { if (currentGUIElementHash != null) { if ("class".equals(atts.getValue("name"))) { currentGUIElementSpec.setType(atts.getValue("value")); } else if ("title".equals(atts.getValue("name"))) { currentGUIElementSpec.setName(atts.getValue("value")); } else if ("path".equals(atts.getValue("name"))) { currentGUIElementSpec.setPath(atts.getValue("value")); } else if ("id".equals(atts.getValue("name"))) { currentGUIElementSpec.setIndex(Integer.parseInt(atts.getValue("value"))); } else if ("parent".equals(atts.getValue("name"))) { currentParentHash = Long.parseLong(atts.getValue("value")); } else if ("position".equals(atts.getValue("name"))) { currentGUIElementSpec.setElementPosition(Integer.parseInt(atts.getValue("value"))); } } else if (currentEventId != null) { if ("source".equals(atts.getValue("name"))) { currentEventSource = Long.parseLong(atts.getValue("value")); } if ("timestamp".equals(atts.getValue("name"))) { currentEventTimestamp = Long.parseLong(atts.getValue("value")); } currentEventParameters.put(atts.getValue("name"), atts.getValue("value")); } } else if (qName.equals("ancestor")) { currentTypeHierarchy.add(atts.getValue("name")); } else if (qName.equals("ancestors")) { currentTypeHierarchy = new LinkedList(); } } /* * (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 { showSteps("end element: " + qName, true); if (qName.equals("sessions")) { if (currentSequence != null) { sequences.add(currentSequence); } } else if (qName.equals("ancestors")) { currentGUIElementSpec.setTypeHierarchy(currentTypeHierarchy); } else if (qName.equals("component") && currentGUIElementHash != null) { try { currentGUIElementTree.add(currentGUIElementHash, currentParentHash, currentGUIElementSpec); } catch (EventTargetModelException e) { throw new SAXException("could not handle GUI element with hash " + currentGUIElementHash + ": " + e.getMessage(), e); } currentGUIElementHash = null; currentParentHash = null; currentTypeHierarchy = null; } else if (currentEventId != null && qName.equals("event")) { IGUIElement currentGUIElement; currentGUIElement = currentGUIElementTree.find(currentEventSource); Event event; // up to now only onClick events are implemented and each // onclick event is processed as a mouse click if (currentGUIElement == null) { } else { event = new Event(instantiateInteraction(currentEventId, currentEventParameters), currentGUIElement); ANDROIDGUIElement currentEventTarget = (ANDROIDGUIElement) event.getTarget(); currentEventTarget.markUsed(); event.setTimestamp(currentEventTimestamp); currentSequence.add(event); } currentEventParameters = null; currentEventId = null; currentEventTimestamp = -1l; } } /** *

* 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(String event, Map eventParameters) throws SAXException { switch (event) { case "onClick": float x = Float.parseFloat(currentEventParameters.get("X")); float y = Float.parseFloat(currentEventParameters.get("Y")); return new TouchSingle(x, y); case "text": return new TextInput(currentEventParameters.get("message"), null); default: throw new SAXException("unhandled event id " + event); } } private void showSteps(String message, Boolean ln) { if (showSteps) { if (ln) { Console.traceln(Level.INFO, message); } else { Console.trace(Level.INFO, message); } } } public void setShowSteps(Boolean showSteps) { this.showSteps = showSteps; } }