// 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.genericevents; 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.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; 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.Locator; 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.HierarchicalEventTargetModel; import de.ugoe.cs.autoquest.eventcore.HierarchicalEventTargetTree; import de.ugoe.cs.autoquest.plugin.genericevents.eventCore.GenericEventTarget; import de.ugoe.cs.autoquest.plugin.genericevents.eventCore.GenericEventTargetFactory; import de.ugoe.cs.autoquest.plugin.genericevents.eventCore.GenericEventTargetSpec; import de.ugoe.cs.util.console.Console; /** *

* This class provides the functionality to parse XML log files generated by monitors of * AutoQUEST. The result of parsing a file is a collection of event sequences and a target model. * This class must be extended by implementing a subclass and the abstract method to complete * the implementation. *

* * @author Patrick Harms * @version 1.0 * */ public abstract class AbstractDefaultLogParser extends DefaultHandler { /** *

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

*/ private Collection> sequences; /** *

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

*/ private HierarchicalEventTargetTree targetTree; /** *

* Id of the event target currently being parsed. *

*/ private String currentTargetId; /** *

* the buffer for event targets already parsed but not processed yet (because e.g. the parent * target has not been parsed yet) *

*/ private List targetBuffer; /** *

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

*/ private String currentEventType; /** *

* the buffer for events already parsed but not processed yet (because e.g. the target * has not been parsed yet) *

*/ private List eventBuffer; /** *

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

*/ private Map currentParameters; /** *

* Internal handle to the sequence currently being parsed. *

*/ private List currentSequence; /** *

* the handle to the locator for correctly throwing exceptions with location information *

*/ private Locator locator; /** *

* Constructor. Creates a new logParser. *

*/ public AbstractDefaultLogParser() { sequences = new LinkedList>(); targetTree = new HierarchicalEventTargetTree (new HierarchicalEventTargetModel(false), new GenericEventTargetFactory()); targetBuffer = new LinkedList(); eventBuffer = new LinkedList(); currentSequence = new LinkedList(); } /** *

* Parses a log file written by an AutoQUEST monitor and creates a collection of event * sequences. *

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

* Parses a log file written by an AutoQUEST monitor and creates a collection of event * sequences. *

* * @param file * file to be parsed * * @throws SAXException in the case, the file could not be parsed */ public void parseFile(File file) throws SAXException { 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.printerrln("Error parsing file " + file.getName()); Console.logException(e); throw new SAXParseException("Error parsing file " + file.getName(), locator, e); } catch (ParserConfigurationException e) { Console.printerrln("Error parsing file " + file.getName()); Console.logException(e); throw new SAXParseException("Error parsing file " + file.getName(), locator, e); } catch (FileNotFoundException e) { Console.printerrln("Error parsing file " + file.getName()); Console.logException(e); throw new SAXParseException("Error parsing file " + file.getName(), locator, e); } // we parse a new file. So clear the buffers. targetBuffer.clear(); eventBuffer.clear(); if (inputSource != null) { inputSource.setSystemId("file://" + file.getAbsolutePath()); try { if (saxParser == null) { throw new RuntimeException("SaxParser creation failed"); } saxParser.parse(inputSource, this); } catch (IOException e) { Console.printerrln("Error parsing file " + file.getName()); Console.logException(e); throw new SAXParseException("Error parsing file " + file.getName(), locator, e); } catch (SAXParseException e) { if ("XML document structures must start and end within the same entity.".equals (e.getMessage())) { // this only denotes, that the final session tag is missing, because the // file wasn't completed. Ignore this but complete the session eventBuffer.add(new BufferEntry("sessionSwitch", null)); processEvents(); } else { throw e; } } } if (targetBuffer.size() > 0) { Console.println(targetBuffer.size() + " GUI elements not processed"); } if (eventBuffer.size() > 0) { Console.printerrln(eventBuffer.size() + " events not processed"); } } /** *

* Returns a collection of event sequences that was obtained from parsing log files. *

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

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

* * @return GUIModel */ public HierarchicalEventTargetModel getHierarchicalEventTargetModel() { return targetTree.getHierarchicalEventTargetModel(); } /* (non-Javadoc) * @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator) */ @Override public void setDocumentLocator(Locator locator) { this.locator = locator; } /* * (non-Javadoc) * * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, * java.lang.String, org.xml.sax.Attributes) */ @Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { if (qName.equals("session")) { // do nothing } else if (qName.equals("target")) { currentTargetId = atts.getValue("id"); currentParameters = new HashMap(); } else if (qName.equals("event")) { currentEventType = atts.getValue("type"); currentParameters = new HashMap(); } else if (qName.equals("param")) { String paramName = atts.getValue("name"); if ((currentTargetId != null) || (currentEventType != null)) { currentParameters.put(paramName, atts.getValue("value")); } else { throw new SAXException("param tag found where it should not be."); } } else { throw new SAXException("unknown tag found: " + qName); } } /* * (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("session") && (eventBuffer != null)) { eventBuffer.add(new BufferEntry("sessionSwitch", null)); processTargets(); processEvents(); } else if (qName.equals("target") && (currentTargetId != null)) { targetBuffer.add(0, new BufferEntry(currentTargetId, currentParameters)); currentTargetId = null; currentParameters = null; } else if (qName.equals("event") && (currentEventType != null)) { eventBuffer.add(new BufferEntry(currentEventType, currentParameters)); currentEventType = null; currentParameters = null; } } /** *

* called to handle parsed targets *

* * @param id the id of the parsed target * @param parameters all parameters parsed for the target * * @return true, if the target could be handled. In this case this method is not called * again for the same target. Otherwise the method is called later again. This * may be required, if a child target is parsed before the parent target */ protected abstract boolean handleTarget(String id, Map parameters) throws SAXException; /** *

* called to handle parsed events *

* * @param type the type of the parsed event * @param parameters the parameters of the parsed event * * @return true, if the event could be handled. In this case this method is not called * again for the same event. Otherwise the method is called later again. This * may be required, if e.g. the target of the event is not yet parsed */ protected abstract boolean handleEvent(String type, Map parameters) throws SAXException; /** *

* returns the tree of parsed targets, which consists of the ids of the targets *

* * @return as described */ protected HierarchicalEventTargetTree getTargetTree() { return targetTree; } /** *

* adds an event to the currently parsed sequence of events *

* * @param event */ protected void addToSequence(Event event) { currentSequence.add(event); } /** *

* this method internally processes targets, that have been parsed but not processed yet. * I.e., for such targets, either the method {@link #handleTarget(String, Map)} has * not been called yet, or it returned false for the previous calls. In this case, the method * is called (again). Furthermore, the processing of events is initiated by a call to * {@link #processEvents()}. *

*/ private void processTargets() throws SAXException { int processedElements = 0; boolean processedElement; do { processedElement = false; // search for the next GUI element that can be processed (use an iterator on the // linked list, as this is most effective) Iterator iterator = targetBuffer.iterator(); while (iterator.hasNext()) { BufferEntry entry = iterator.next(); if (handleTarget(entry.id, entry.parameters)) { iterator.remove(); processedElements++; processedElement = true; } } } while (processedElement); if (processedElements > 0) { processEvents(); } } /** *

* this method internally processes events, that have been parsed but not processed yet. * I.e., for such events, either the method {@link #handleEvent(String, Map)} has * not been called yet, or it returned false for the previous calls. In this case, the method * is called (again). *

*/ private void processEvents() throws SAXException { boolean processedEvent; do { processedEvent = false; // check, if the next event can be processed if (eventBuffer.size() > 0) { BufferEntry entry = eventBuffer.get(0); if ((entry != null) && (entry.id != null) && (entry.parameters != null)) { processedEvent = handleEvent(entry.id, entry.parameters); if (processedEvent) { eventBuffer.remove(0); } } else { // the entry signals a session switch. Close the current session and start the // next one if (currentSequence.size() > 0) { sequences.add(currentSequence); currentSequence = new LinkedList(); } eventBuffer.remove(0); processedEvent = true; } } } while (processedEvent); } /** *

* This class is used internally for storing events and targets in lists. *

*/ private static class BufferEntry { /** */ private String id; /** */ private Map parameters; /** * */ private BufferEntry(String id, Map parameters) { this.id = id; this.parameters = parameters; } } }