// 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.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; 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.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import de.ugoe.cs.util.StringTools; import de.ugoe.cs.util.console.Console; /** *

* corrects older JFC log files which sometimes do not contain correct source specifications for * events. It parses the file and adds component specifications to the sources, that do not have * them. For each invalid source it checks, if there is another source with the same * toString parameter but a complete list of components. If one is found, it is reused * as source for the event with the wrong source. If none is found, a new component list is * generated. This contains as parent components the components of the source of the previous event. * The leaf component is parsed from the toString parameter that is provided with the * source specifications. The resulting leaf nodes are not fully correct. They may pretend to * be equal although they are not. Furthermore they may resist at a position in the GUI tree where * they are not in reality. But more correctness is not achievable based on the * toString parameter. *

* * @version $Revision: $ $Date: 05.09.2012$ * @author 2012, last modified by $Author: pharms$ */ public class JFCTraceCorrector extends DefaultHandler { /** *

* the file to write the result into *

*/ private PrintStream outFile; /** *

* the currently parsed event *

*/ private Event currentEvent; /** *

* the currently parsed source of the currently parsed event *

*/ private Source currentSource; /** *

* the list of all sources parsed in a file identified through their toString * representation *

*/ private Map> allSources = new HashMap>(); /** *

* the currently parsed component of the currently parsed source of the currently parsed event *

*/ private Component currentComponent; /** *

* the currently parsed session *

*/ private Session currentSession; /** *

* corrects the given file and returns the name of the file into which the result was written *

* * @param filename the name of the file to be corrected * * @return the name of the file with the corrected logfile * * @throws IllegalArgumentException if the filename is null */ public String correctFile(String filename) throws IllegalArgumentException { if (filename == null) { throw new IllegalArgumentException("filename must not be null"); } return correctFile(new File(filename)).getAbsolutePath(); } /** *

* corrects the given file, stores the result in the second provided file and returns the * name of the file into which the result was written *

* * @param filename the name of the file to be corrected * @param resultFile the name of the file into which the corrected log shall be written * * @return the name of the file with the corrected logfile * * @throws IllegalArgumentException if the filename or resultFile is null */ public String correctFile(String filename, String resultFile) throws IllegalArgumentException { if ((filename == null) | (resultFile == null)) { throw new IllegalArgumentException("filename and resultFile must not be null"); } return correctFile(new File(filename), new File(resultFile)).getAbsolutePath(); } /** *

* corrects the given file and returns the file into which the result was written. The name * of the resulting file is contains the suffix "_corrected" before the dot. *

* * @param file the file to be corrected * * @return the file containing the corrected logfile * * @throws IllegalArgumentException if the file is null */ public File correctFile(File file) throws IllegalArgumentException { if (file == null) { throw new IllegalArgumentException("file must not be null"); } int index = file.getName().lastIndexOf('.'); String fileName = file.getName().substring(0, index) + "_corrected" + file.getName().substring(index); File resultFile = new File(file.getParentFile(), fileName); return correctFile(file, resultFile); } /** *

* corrects the given file, stores the result in the second provided file and returns the * file into which the result was written *

* * @param file the file to be corrected * @param resultFile the file into which the corrected log shall be written * * @return the file with the corrected logfile * * @throws IllegalArgumentException if the file or resultFile is null or if they are equal */ public File correctFile(File file, File resultFile) throws IllegalArgumentException { if ((file == null) || (resultFile == null)) { throw new IllegalArgumentException("file and result file must not be null"); } if (file.getAbsolutePath().equals(resultFile.getAbsolutePath())) { throw new IllegalArgumentException("file and result file must not be equal"); } try { outFile = new PrintStream(new BufferedOutputStream(new FileOutputStream(resultFile))); outFile.println(""); } catch (FileNotFoundException e1) { throw new IllegalArgumentException("could not create a corrected file name " + resultFile + " next to " + file); } 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 null; } catch (ParserConfigurationException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return null; } catch (SAXException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return null; } catch (FileNotFoundException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return null; } 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 null; } catch (SAXException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return null; } catch (IOException e) { Console.printerr("Error parsing file + " + file.getName()); Console.logException(e); return null; } } if (outFile != null) { outFile.close(); } return resultFile; } /* * (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")) { if (currentSession != null) { throw new SAXException("nested sessions are not allowed"); } currentSession = new Session("sessions"); } else if (qName.equals("newsession")) { if (currentSession != null) { currentSession.dump(outFile); } currentSession = new Session("newsession"); } else if (qName.equals("event")) { if (currentEvent != null) { throw new SAXException("nested events are not allowed"); } currentEvent = new Event(atts.getValue("id")); } else if (qName.equals("source")) { if (currentSource != null) { throw new SAXException("nested sources are not allowed"); } currentSource = new Source(); } else if (qName.equals("component")) { if (currentComponent != null) { throw new SAXException("nested components are not allowed"); } currentComponent = new Component(); } else if (qName.equals("param")) { if (currentComponent != null) { currentComponent.addParameter(atts.getValue("name"), atts.getValue("value")); } else if (currentSource != null) { currentSource.addParameter(atts.getValue("name"), atts.getValue("value")); } else if (currentEvent != null) { currentEvent.addParameter(atts.getValue("name"), atts.getValue("value")); } else { throw new SAXException("parameter occurred at an unexpected place"); } } else { throw new SAXException("unexpected tag " + 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 { // we do not have to check, if the current objects are null, as the Parser itself will // throw an exception, if there is a closing tag for a non existent opening tag. But // with a correct opening tag, the member variables will not be null (see startElement()) if (qName.equals("sessions")) { correctSources(currentSession); currentSession.dump(outFile); currentSession = null; allSources.clear(); } else if (qName.equals("newsession")) { correctSources(currentSession); currentSession.dump(outFile); currentSession = null; allSources.clear(); } else if (qName.equals("event")) { currentSession.addEvent(currentEvent); currentEvent = null; } else if (qName.equals("source")) { currentEvent.setSource(getUniqueSource(currentSource)); currentSource = null; } else if (qName.equals("component")) { currentSource.addComponent(currentComponent); currentComponent = null; } else if (!qName.equals("param")) { throw new SAXException("unexpected closing tag " + qName); } } /** *

* returns a source object, that is equal to the provided one but that is unique throughout * the parsing process. The method may return the provided source, if this is the first * occurrence of this source. This method is needed to reduce the amount of source * representations that are instantiated during parsing log files. *

* * @param source the source to search for a unique representation * * @return the unique representation of the source */ private Source getUniqueSource(Source source) { Source existingSource = null; List candidates = allSources.get(source.getToStringValue()); if (candidates != null) { for (Source candidate : candidates) { if (candidate.equals(source)) { existingSource = candidate; break; } } } if (existingSource == null) { if (candidates == null) { candidates = new ArrayList(); allSources.put(source.getToStringValue(), candidates); } candidates.add(source); existingSource = source; } return existingSource; } /** *

* convenience method to find a source based on its toString parameter value. * The method only returns sources, which match the provided toString * representation and which have a valid list of components (size greater 0). *

* * @param toStringValue the value of the toString parameter the source to find * must have * * @return the source matching the parameter and having a valid list of components or null if * none is found */ private Source findValidSource(String toStringValue) { Source existingSource = null; List candidates = allSources.get(toStringValue); if (candidates != null) { for (Source candidate : candidates) { if ((candidate.getComponents() != null) && (candidate.getComponents().size() > 0)) { existingSource = candidate; break; } } } return existingSource; } /** *

* corrects all wrong sources in the events of the session. For each wrong resource, the * {@link #correctEventSource(Event, Source)} method is called. *

* * @param session the session of which the events shall be corrected */ private void correctSources(Session session) { Source previousSource = null; for (Event event : session.getEvents()) { if ((event.getSource() == null) || (event.getSource().getComponents() == null) || (event.getSource().getComponents().size() == 0)) { correctEventSource(event, previousSource); } previousSource = event.getSource(); } } /** *

* corrects the source of an event. It first searches for a correct source with an equal * toString parameter. If there is any, this is reused. Otherwise it creates a * new correct source. For this, it copies all parameters of the source of the provided previous * event if they are not included in the source already. Furthermore, it adds all components * of the source of the previous event. At last, it adds a further component based on the * information found in the toString parameter of the source. *

* * @param event the event of which the source must be corrected * @param previousSource the source of the previous event to be potentially reused partially */ private void correctEventSource(Event event, Source previousSource) { Source existingSource = null; if ((event.getSource() != null) && (event.getSource().getToStringValue() != null)) { existingSource = findValidSource(event.getSource().getToStringValue()); } if (existingSource != null) { event.setSource(existingSource); } else { if (previousSource != null) { for (Parameter parameterOfPreviousSource : previousSource.getParameters()) { boolean foundParameter = false; for (Parameter parameter : event.getSource().getParameters()) { if (parameter.getName().equals(parameterOfPreviousSource.getName())) { foundParameter = true; break; } } if (!foundParameter) { event.getSource().addParameter(parameterOfPreviousSource); } } for (Component component : previousSource.getComponents()) { if (!(component instanceof ComponentFromToString)) { event.getSource().addComponent(component); } } } event.getSource().addComponent (getComponentFromToString(event.getSource().getToStringValue())); } } /** *

* determines a component based on the toString parameter of a source. * For this, it parses the parameter value and tries to determine several infos such as the * type and the name of it. The resulting components are not always distinguishable. This is, * because the toString parameter does not contain sufficient information for * correct identification. *

* * @param toStringValue the toString parameter of a source * * @return the component parsed from the toString parameter */ private Component getComponentFromToString(String toStringValue) { ComponentFromToString component = new ComponentFromToString(); // search for the beginning of the parameters section. Up to this position we find the class int start = toStringValue.indexOf('['); String clazz = toStringValue.substring(0, start); // the first parameters are x and y coordinate as well as the size. The size is one // parameter, where with and height are separated with an 'x' start = toStringValue.indexOf(',', start) + 1; int end = toStringValue.indexOf(',', start); component.setX(Integer.parseInt(toStringValue.substring(start, end))); start = end + 1; end = toStringValue.indexOf(',', start); component.setY(Integer.parseInt(toStringValue.substring(start, end))); start = end + 1; end = toStringValue.indexOf('x', start); component.setWidth(Integer.parseInt(toStringValue.substring(start, end))); start = end + 1; end = toStringValue.indexOf(',', start); component.setHeight(Integer.parseInt(toStringValue.substring(start, end))); // no start parsing the rest of the parameters and extract those having a key and a // value and whose key is text, defaultIcon, or an alignment int intermediate; start = end + 1; String title = null; String icon = null; String alignment = null; do { end = toStringValue.indexOf(',', start); intermediate = toStringValue.indexOf('[', start); if ((intermediate >= 0) && (intermediate < end)) { // the value of the parameter itself contains brackets. So try to determine the // real end of the parameter end = toStringValue.indexOf(']', intermediate); end = toStringValue.indexOf(',', end); } if (end < 0) { //we reached the end of the stream. So the the end to the "end" end = toStringValue.lastIndexOf(']'); } intermediate = toStringValue.indexOf('=', start); if ((intermediate >= 0) && (intermediate < end)) { // this is a key value pair, so store the the parameter String key = toStringValue.substring(start, intermediate); String value = toStringValue.substring(intermediate + 1, end); if ("text".equals(key)) { title = value; } else if ("defaultIcon".equals(key)) { icon = value; } else if ("alignmentX".equals(key) || "alignmentY".equals(key)) { if (alignment == null) { alignment = value; } else { alignment += "/" + value; } } } /*else { // this is a simple value, for now simply ignore it String key = toStringValue.substring(start, end); if (!"invalid".equals(key)) { componentHash += key.hashCode(); component.params.add(new String[] { key, "true" }); } }*/ start = end + 1; } while (start < toStringValue.lastIndexOf(']')); // finish the component specification by setting the parameters if ((title == null) || "".equals(title) || "null".equals(title)) { if ((icon == null) || "".equals(icon) || "null".equals(icon)) { title = clazz.substring(clazz.lastIndexOf('.') + 1) + "("; // to be able to distinguish some elements, that usually have no name and icon, try // to include some of their specific identifying information in their name. if ("org.tigris.gef.presentation.FigTextEditor".equals(clazz) || "org.argouml.core.propertypanels.ui.UMLTextField".equals(clazz)) { title += "height " + component.height + ", "; } else if ("org.argouml.core.propertypanels.ui.UMLLinkedList".equals(clazz) || "org.argouml.core.propertypanels.ui.LabelledComponent".equals(clazz)) { title += "position " + component.getX() + "/" + component.getY() + ", "; } title += "alignment " + alignment + ")"; } else { // to be able to distinguish some elements, that usually have no name but an icon, // try to include some of their specific identifying information in their name. if ("org.tigris.toolbar.toolbutton.PopupToolBoxButton".equals(clazz)) { icon = icon.substring(0, icon.lastIndexOf('@')); title = clazz.substring(clazz.lastIndexOf('.') + 1) + "(position " + component.getX() + ")"; } else { title = icon; } } } component.addParameter("title", title); component.addParameter("class", clazz); component.addParameter("icon", ((icon == null) ? "" : icon)); component.addParameter("index", "-1"); int hashCode = clazz.hashCode() + title.hashCode(); if (hashCode < 0) { hashCode = -hashCode; } component.addParameter("hash", Integer.toString(hashCode, 16)); return component; } /** *

* used to dump a list of parameters to the provided print stream *

*/ private void dumpParams(PrintStream out, List params, String indent) { for (Parameter param : params) { out.print(indent); out.print(""); } } /** *

* check if two parameter lists are equal. Thea are equal if the contain the same parameters * ignoring their order. *

* * @param params1 the first parameter list to be compared * @param params2 the second parameter list to be compared * * @return true if both lists contain the same parameters, false else. */ private boolean parametersEqual(List params1, List params2) { if (params1 == null) { return params2 == null; } if (params2 == null) { return false; } if (params1.size() != params2.size()) { return false; } boolean found; for (Parameter param1 : params1) { found = false; for (Parameter param2 : params2) { if (param1.equals(param2)) { found = true; break; } } if (!found) { return false; } } return true; } /** *

* used to carry all events of a session and to dump it to the output file *

*/ private class Session { /** */ private String type; /** */ private List events = new ArrayList(); /** * */ private Session(String type) { if (type == null) { throw new IllegalArgumentException("type must not be null"); } this.type = type; } /** * */ private void addEvent(Event event) { if (event == null) { throw new IllegalArgumentException("event must not be null"); } events.add(event); } /** * @return the events */ private List getEvents() { return Collections.unmodifiableList(events); } /** * */ private void dump(PrintStream out) { if (out == null) { throw new IllegalArgumentException("out must not be null"); } out.print("<"); out.print(type); out.println(">"); for (Event event : events) { event.dump(out); } out.print(""); } } /** *

* used to carry all information about an event and to dump it to the output file *

*/ private class Event { /** */ private String id; /** */ private List params = new ArrayList(); /** */ private Source source; /** * */ private Event(String id) { if (id == null) { throw new IllegalArgumentException("id must not be null"); } this.id = id; } /** * @param source the source to set */ private void setSource(Source source) { this.source = source; } /** * @return the source */ private Source getSource() { return source; } /** * */ private void addParameter(String name, String value) { if (name == null) { throw new IllegalArgumentException("name must not be null"); } if (value == null) { throw new IllegalArgumentException("value must not be null"); } params.add(new Parameter(name, value)); } /** * */ private void dump(PrintStream out) { if (out == null) { throw new IllegalArgumentException("out must not be null"); } out.print(""); dumpParams(out, params, " "); source.dump(out); out.println(""); } } /** *

* used to carry all information about a source of an event and to dump it to the output file *

*/ private class Source { /** */ private List params = new ArrayList(); /** */ private List components = new ArrayList(); /** */ private String toStringValue = null; /** * */ private String getToStringValue() { if (toStringValue == null) { for (Parameter param : params) { if (("toString".equals(param.getName())) && (param.getValue() != null) && (!"".equals(param.getValue()))) { toStringValue = param.getValue(); break; } } } return toStringValue; } /** * */ private void addParameter(String name, String value) { if (name == null) { throw new IllegalArgumentException("name must not be null"); } if (value == null) { throw new IllegalArgumentException("value must not be null"); } params.add(new Parameter(name, value)); } /** * */ private void addParameter(Parameter parameter) { if (parameter == null) { throw new IllegalArgumentException("parameter must not be null"); } params.add(parameter); } /** * @return the params */ private List getParameters() { return Collections.unmodifiableList(params); } /** * */ private void addComponent(Component component) { if (component == null) { throw new IllegalArgumentException("component must not be null"); } components.add(component); } /** * @return the components */ private List getComponents() { return Collections.unmodifiableList(components); } /** * */ private void dump(PrintStream out) { if (out == null) { throw new IllegalArgumentException("out must not be null"); } out.println(" "); dumpParams(out, params, " "); for (Component component : components) { component.dump(out); } out.println(" "); } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Source) { Source other = (Source) obj; if ((getToStringValue() != other.getToStringValue()) || ((getToStringValue() != null) && (!getToStringValue().equals(other.getToStringValue())))) { return false; } if (!parametersEqual(params, other.params)) { return false; } if (components == null) { return other.components == null; } if (other.components == null) { return false; } if (components.size() != other.components.size()) { return false; } for (int i = 0; i < components.size(); i++) { if (!components.get(i).equals(other.components.get(i))) { return false; } } return true; } return false; } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { String str = getToStringValue(); if (str != null) { return str.hashCode(); } else { // ensure that all incomplete sources provide the same hashcode return 0; } } } /** *

* used to carry all information about a component of a source and to dump it to the output file *

*/ private class Component { /** */ private List params = new ArrayList(); /** * */ protected void addParameter(String name, String value) { if (name == null) { throw new IllegalArgumentException("name must not be null"); } if (value == null) { throw new IllegalArgumentException("value must not be null"); } params.add(new Parameter(name, value)); } /** * @return the params */ private List getParameters() { return Collections.unmodifiableList(params); } /** * */ protected void dump(PrintStream out) { if (out == null) { throw new IllegalArgumentException("out must not be null"); } out.println(" "); dumpParams(out, params, " "); out.println(" "); } /** * */ public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Component) { return parametersEqual(params, ((Component) obj).params); } else { return false; } } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { // all components with an equally sized parameter list can be equal. This does not // work, if not all component parameters are set yet. But we do not use components // in a hash map so we provide an easy implementation return params.size(); } } /** *

* represents a specific component, which was read from the toString parameter of a source *

*/ private class ComponentFromToString extends Component { /** */ private int x; /** */ private int y; /** */ private int width; /** */ private int height; /** * @param x the x to set */ private void setX(int x) { this.x = x; } /** * @return the x */ private int getX() { return x; } /** * @param y the y to set */ private void setY(int y) { this.y = y; } /** * @return the y */ private int getY() { return y; } /** * @param width the width to set */ private void setWidth(int width) { this.width = width; } /** * @param height the height to set */ private void setHeight(int height) { this.height = height; } /** * */ @Override protected void dump(PrintStream out) { if (out == null) { throw new IllegalArgumentException("out must not be null"); } out.println(" "); out.print(" "); out.print(""); out.print(" "); out.print(""); out.print(" "); out.print(""); out.print(" "); out.print(""); dumpParams(out, super.getParameters(), " "); out.println(" "); } /** * */ public boolean equals(Object obj) { if (!super.equals(obj)) { return false; } if (obj instanceof ComponentFromToString) { ComponentFromToString other = (ComponentFromToString) obj; return (x == other.x) && (y == other.y) && (width == other.width) && (height == other.height); } else { return false; } } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return super.hashCode() + x + y + width + height; } } /** *

* used to carry all information about a parameter being a key and a value *

*/ private class Parameter { /** */ private String name; /** */ private String value; /** * */ private Parameter(String name, String value) { if (name == null) { throw new IllegalArgumentException("name must not be null"); } if (value == null) { throw new IllegalArgumentException("value must not be null"); } this.name = name; this.value = value; } /** * @return the name */ private String getName() { return name; } /** * @return the value */ private String getValue() { return value; } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (obj instanceof Parameter) { return (name.equals(((Parameter) obj).name) && value.equals(((Parameter) obj).value)); } else { return false; } } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return name.hashCode() + value.hashCode(); } } }