From 08a530ef53cc4756f5e632b69c78830872ebd9f4 Mon Sep 17 00:00:00 2001 From: Chris Jaekl Date: Thu, 3 Sep 2015 22:47:45 +0900 Subject: [PATCH] Partial implementation of XML parse for FindBugs output --- prod/net/jaekl/cfb/analyze/Analysis.java | 5 + prod/net/jaekl/cfb/analyze/Analyzer.java | 99 ++++ prod/net/jaekl/cfb/util/Command.java | 98 ++++ prod/net/jaekl/cfb/util/Util.java | 13 + prod/net/jaekl/cfb/xml/BugClass.java | 32 ++ prod/net/jaekl/cfb/xml/BugCollection.java | 32 ++ prod/net/jaekl/cfb/xml/BugInstance.java | 34 ++ prod/net/jaekl/cfb/xml/BugMethod.java | 32 ++ prod/net/jaekl/cfb/xml/SourceLine.java | 61 ++ prod/net/jaekl/qd/QDException.java | 14 + .../jaekl/qd/xml/MismatchedTagsException.java | 21 + .../qd/xml/MissingAttributeException.java | 14 + .../jaekl/qd/xml/MissingInfoException.java | 47 ++ prod/net/jaekl/qd/xml/ParseErrorHandler.java | 29 + prod/net/jaekl/qd/xml/ParseHandler.java | 123 ++++ prod/net/jaekl/qd/xml/ParseResult.java | 183 ++++++ prod/net/jaekl/qd/xml/XmlParseException.java | 17 + test/net/jaekl/cfb/analyze/AnalyzerTest.java | 36 ++ .../qd/xml/MissingInfoExceptionTest.java | 40 ++ test/net/jaekl/qd/xml/ParseResultTest.java | 524 ++++++++++++++++++ 20 files changed, 1454 insertions(+) create mode 100644 prod/net/jaekl/cfb/analyze/Analysis.java create mode 100644 prod/net/jaekl/cfb/analyze/Analyzer.java create mode 100644 prod/net/jaekl/cfb/util/Command.java create mode 100644 prod/net/jaekl/cfb/util/Util.java create mode 100644 prod/net/jaekl/cfb/xml/BugClass.java create mode 100644 prod/net/jaekl/cfb/xml/BugCollection.java create mode 100644 prod/net/jaekl/cfb/xml/BugInstance.java create mode 100644 prod/net/jaekl/cfb/xml/BugMethod.java create mode 100644 prod/net/jaekl/cfb/xml/SourceLine.java create mode 100644 prod/net/jaekl/qd/QDException.java create mode 100644 prod/net/jaekl/qd/xml/MismatchedTagsException.java create mode 100644 prod/net/jaekl/qd/xml/MissingAttributeException.java create mode 100644 prod/net/jaekl/qd/xml/MissingInfoException.java create mode 100644 prod/net/jaekl/qd/xml/ParseErrorHandler.java create mode 100644 prod/net/jaekl/qd/xml/ParseHandler.java create mode 100644 prod/net/jaekl/qd/xml/ParseResult.java create mode 100644 prod/net/jaekl/qd/xml/XmlParseException.java create mode 100644 test/net/jaekl/cfb/analyze/AnalyzerTest.java create mode 100644 test/net/jaekl/qd/xml/MissingInfoExceptionTest.java create mode 100644 test/net/jaekl/qd/xml/ParseResultTest.java diff --git a/prod/net/jaekl/cfb/analyze/Analysis.java b/prod/net/jaekl/cfb/analyze/Analysis.java new file mode 100644 index 0000000..e8a938a --- /dev/null +++ b/prod/net/jaekl/cfb/analyze/Analysis.java @@ -0,0 +1,5 @@ +package net.jaekl.cfb.analyze; + +public class Analysis { + +} diff --git a/prod/net/jaekl/cfb/analyze/Analyzer.java b/prod/net/jaekl/cfb/analyze/Analyzer.java new file mode 100644 index 0000000..715cae9 --- /dev/null +++ b/prod/net/jaekl/cfb/analyze/Analyzer.java @@ -0,0 +1,99 @@ +package net.jaekl.cfb.analyze; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.MessageFormat; +import java.util.Locale; +import java.util.Locale.Category; + +import net.jaekl.cfb.CfbBundle; +import net.jaekl.cfb.util.Command; + +public class Analyzer { + File m_findbugsDir; + + public Analyzer(File findbugsDir) { + m_findbugsDir = findbugsDir; + } + + public Analysis analyze(PrintWriter pw, File workDir, File fbp) throws IOException { + Analysis result = new Analysis(); + + File fbOutput = outputWorkFile(workDir, fbp); + + String cmdLine = buildCommandLine(workDir, fbp, fbOutput); + pw.println(cmdLine); + pw.flush(); + Command.Result fbResult = new Command().exec(cmdLine); + if (0 != fbResult.getRetCode()) { + // Our attempt to execute FindBugs failed. + // Report the error and return null. + String cannotExecFormat = trans(CfbBundle.CANNOT_EXEC); + String cannotExecMsg = MessageFormat.format(cannotExecFormat, cmdLine, fbResult.getRetCode()); + pw.println(cannotExecMsg); + pw.println(trans(CfbBundle.STDOUT_WAS)); + pw.println(fbResult.getStdout()); + pw.println(trans(CfbBundle.STDERR_WAS)); + pw.println(fbResult.getStderr()); + return null; + } + + result = parseFbOutput(fbOutput); + + return result; + } + + String trans(String key) { + return CfbBundle.getInst(Locale.getDefault(Category.DISPLAY)).get(key); + } + + String buildCommandLine(File workDir, File fbp, File fbOutput) + { + assert(null != workDir); + assert(null != fbp); + assert(null != fbOutput); + + StringBuilder sb = new StringBuilder(); + + sb.append(m_findbugsDir.getAbsolutePath()) + .append(File.separator) + .append("bin") + .append(File.separator) + .append("findbugs -textui -xml -output ") + .append(fbOutput.getAbsolutePath()) + .append(" -project ") + .append(fbp.getAbsolutePath()); + + return sb.toString(); + } + + // Come up with an appropriate name for the XML output file. + // workDir: place where the file should be created + // fbp: FindBugsProject file + File outputWorkFile(File workDir, File fbp) + { + assert(null != workDir); + assert(null != fbp); + + String workPath = workDir.getAbsolutePath(); + + String projName = fbp.getName(); + int len = projName.length(); + if (len > 4) { + String extension = projName.substring(len - 4, len).toLowerCase(Locale.CANADA); + if (extension.equals(".fbp")) { + projName = projName.substring(0, len - 4); + } + } + + return new File(workPath + File.separator + projName + ".xml"); + } + + // Parse the output.xml that resulted from a FindBugs run, + // and store its findings into an Analysis object. + Analysis parseFbOutput(File fbOutput) + { + return null; + } +} diff --git a/prod/net/jaekl/cfb/util/Command.java b/prod/net/jaekl/cfb/util/Command.java new file mode 100644 index 0000000..4242eea --- /dev/null +++ b/prod/net/jaekl/cfb/util/Command.java @@ -0,0 +1,98 @@ +package net.jaekl.cfb.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +public class Command { + public static class Result + { + private int m_retCode; + private String m_stdout; + private String m_stderr; + + Result(int retCode, String stdout, String stderr) { + m_retCode = retCode; + m_stdout = stdout; + m_stderr = stderr; + } + + public int getRetCode() { return m_retCode; } + public String getStdout() { return m_stdout; } + public String getStderr() { return m_stderr; } + } + + private static class StreamGobbler extends Thread { + + private StringBuilder m_sb; + private BufferedReader m_br; + + public StreamGobbler(InputStream is) { + m_sb = new StringBuilder(); + m_br = new BufferedReader(new InputStreamReader(is)); + } + + @Override + public void run() { + String line; + try { + line = m_br.readLine(); + while (null != line) { + m_sb.append(line).append(System.lineSeparator()); + line = m_br.readLine(); + } + } catch (IOException exc) { + m_sb.append(Util.stringify(exc)); + } + finally { + try { + m_br.close(); + } catch (IOException exc) { + m_sb.append(Util.stringify(exc)); + } + } + } + + public String getOutput() { return m_sb.toString(); } + } + + public Command() + { + // no-op + } + + public Result exec(String cmd) throws IOException + { + int retCode = 0; + String stdout = ""; + String stderr = ""; + + Process proc = doRuntimeExec(cmd); + assert( null != proc ); + + StreamGobbler stdoutGobbler = new StreamGobbler(proc.getInputStream()); + StreamGobbler stderrGobbler = new StreamGobbler(proc.getErrorStream()); + + stdoutGobbler.start(); + stderrGobbler.start(); + + try { + retCode = proc.waitFor(); + stdoutGobbler.join(); + stderrGobbler.join(); + + stdout = stdoutGobbler.getOutput(); + stderr = stdoutGobbler.getOutput(); + + } catch (InterruptedException exc) { + stderr += Util.stringify(exc); + } + + return new Result(retCode, stdout, stderr); + } + + Process doRuntimeExec(String cmd) throws IOException { + return Runtime.getRuntime().exec(cmd); + } +} diff --git a/prod/net/jaekl/cfb/util/Util.java b/prod/net/jaekl/cfb/util/Util.java new file mode 100644 index 0000000..c0e7b1c --- /dev/null +++ b/prod/net/jaekl/cfb/util/Util.java @@ -0,0 +1,13 @@ +package net.jaekl.cfb.util; + +import java.io.PrintWriter; +import java.io.StringWriter; + +public class Util { + public static String stringify(Throwable thr) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + thr.printStackTrace(pw); + return sw.toString(); + } +} diff --git a/prod/net/jaekl/cfb/xml/BugClass.java b/prod/net/jaekl/cfb/xml/BugClass.java new file mode 100644 index 0000000..34e02a2 --- /dev/null +++ b/prod/net/jaekl/cfb/xml/BugClass.java @@ -0,0 +1,32 @@ +package net.jaekl.cfb.xml; + +import org.xml.sax.Attributes; + +import net.jaekl.qd.xml.ParseResult; +import net.jaekl.qd.xml.XmlParseException; + +public class BugClass extends ParseResult { + + static final String ROOT_TAG = "Class"; + static final String[] INTERNAL = { }; + static final Object[][] EXTERNAL = { { SourceLine.ROOT_TAG, SourceLine.class} }; + + public BugClass() { + super(ROOT_TAG, INTERNAL, EXTERNAL); + } + + @Override + public void endContents(String uri, String localName, String qName, + String chars, Attributes attr) throws XmlParseException { + // TODO Auto-generated method stub + + } + + @Override + public void endExternal(String uri, String localName, String qName) + throws XmlParseException { + // TODO Auto-generated method stub + + } + +} diff --git a/prod/net/jaekl/cfb/xml/BugCollection.java b/prod/net/jaekl/cfb/xml/BugCollection.java new file mode 100644 index 0000000..9078e2f --- /dev/null +++ b/prod/net/jaekl/cfb/xml/BugCollection.java @@ -0,0 +1,32 @@ +package net.jaekl.cfb.xml; + +import org.xml.sax.Attributes; + +import net.jaekl.qd.xml.ParseResult; +import net.jaekl.qd.xml.XmlParseException; + +public class BugCollection extends ParseResult { + + static final String ROOT_TAG = "BugCollection"; + static final String[] INTERNAL = { }; + static final Object[][] EXTERNAL = { { BugInstance.ROOT_TAG, BugInstance.class} }; + + public BugCollection() { + super(ROOT_TAG, INTERNAL, EXTERNAL); + } + + @Override + public void endContents(String uri, String localName, String qName, + String chars, Attributes attr) throws XmlParseException { + // TODO Auto-generated method stub + + } + + @Override + public void endExternal(String uri, String localName, String qName) + throws XmlParseException { + // TODO Auto-generated method stub + + } + +} diff --git a/prod/net/jaekl/cfb/xml/BugInstance.java b/prod/net/jaekl/cfb/xml/BugInstance.java new file mode 100644 index 0000000..81ebcfc --- /dev/null +++ b/prod/net/jaekl/cfb/xml/BugInstance.java @@ -0,0 +1,34 @@ +package net.jaekl.cfb.xml; + +import org.xml.sax.Attributes; + +import net.jaekl.qd.xml.ParseResult; +import net.jaekl.qd.xml.XmlParseException; + +public class BugInstance extends ParseResult { + + static final String ROOT_TAG = "BugInstance"; + static final String[] INTERNAL = { }; + static final Object[][] EXTERNAL = { { BugClass.ROOT_TAG, BugClass.class}, + { BugMethod.ROOT_TAG, BugMethod.class}, + { SourceLine.ROOT_TAG, SourceLine.class} }; + + public BugInstance() { + super(ROOT_TAG, INTERNAL, EXTERNAL); + } + + @Override + public void endContents(String uri, String localName, String qName, + String chars, Attributes attr) throws XmlParseException { + // TODO Auto-generated method stub + + } + + @Override + public void endExternal(String uri, String localName, String qName) + throws XmlParseException { + // TODO Auto-generated method stub + + } + +} diff --git a/prod/net/jaekl/cfb/xml/BugMethod.java b/prod/net/jaekl/cfb/xml/BugMethod.java new file mode 100644 index 0000000..6a63b43 --- /dev/null +++ b/prod/net/jaekl/cfb/xml/BugMethod.java @@ -0,0 +1,32 @@ +package net.jaekl.cfb.xml; + +import org.xml.sax.Attributes; + +import net.jaekl.qd.xml.ParseResult; +import net.jaekl.qd.xml.XmlParseException; + +public class BugMethod extends ParseResult { + + static final String ROOT_TAG = "Method"; + static final String[] INTERNAL = { }; + static final Object[][] EXTERNAL = { { SourceLine.ROOT_TAG, SourceLine.class} }; + + public BugMethod() { + super(ROOT_TAG, INTERNAL, EXTERNAL); + } + + @Override + public void endContents(String uri, String localName, String qName, + String chars, Attributes attr) throws XmlParseException { + // TODO Auto-generated method stub + + } + + @Override + public void endExternal(String uri, String localName, String qName) + throws XmlParseException { + // TODO Auto-generated method stub + + } + +} diff --git a/prod/net/jaekl/cfb/xml/SourceLine.java b/prod/net/jaekl/cfb/xml/SourceLine.java new file mode 100644 index 0000000..c78ae3a --- /dev/null +++ b/prod/net/jaekl/cfb/xml/SourceLine.java @@ -0,0 +1,61 @@ +package net.jaekl.cfb.xml; + +import org.xml.sax.Attributes; + +import net.jaekl.qd.xml.MissingAttributeException; +import net.jaekl.qd.xml.ParseResult; +import net.jaekl.qd.xml.XmlParseException; + +public class SourceLine extends ParseResult { + + static final String ROOT_TAG = "SourceLine"; + static final String[] INTERNAL = { }; + static final Object[][] EXTERNAL = { }; + + static final String ATTR_CLASS_NAME = "classname"; + static final String ATTR_START = "start"; + static final String ATTR_END = "end"; + + String m_className; + int m_start; + int m_end; + + public SourceLine() { + super(ROOT_TAG, INTERNAL, EXTERNAL); + m_className = null; + m_start = m_end = (-1); + } + + @Override + public void endContents(String uri, String localName, String qName, String chars, Attributes attr) + throws XmlParseException + { + String scratch; + + m_className = getRequiredAttr(localName, attr, ATTR_CLASS_NAME); + + scratch = getRequiredAttr(localName, attr, ATTR_START); + m_start = Integer.parseInt(scratch); + + scratch = getRequiredAttr(localName, attr, ATTR_END); + m_end = Integer.parseInt(scratch); + } + + String getRequiredAttr(String tagName, Attributes attr, String attrName) + throws MissingAttributeException + { + String result = attr.getValue(attrName); + if (null == result) { + throw new MissingAttributeException(tagName, attrName); + } + return result; + } + + @Override + public void endExternal(String uri, String localName, String qName) + throws XmlParseException + { + // no-op + } + +} diff --git a/prod/net/jaekl/qd/QDException.java b/prod/net/jaekl/qd/QDException.java new file mode 100644 index 0000000..f5478c1 --- /dev/null +++ b/prod/net/jaekl/qd/QDException.java @@ -0,0 +1,14 @@ +package net.jaekl.qd; + +public class QDException extends Exception +{ + private static final long serialVersionUID = 1L; + + public QDException() { + super(); + } + + public QDException(Throwable t) { + super(t); + } +} diff --git a/prod/net/jaekl/qd/xml/MismatchedTagsException.java b/prod/net/jaekl/qd/xml/MismatchedTagsException.java new file mode 100644 index 0000000..9c8b69c --- /dev/null +++ b/prod/net/jaekl/qd/xml/MismatchedTagsException.java @@ -0,0 +1,21 @@ +// Copyright (C) 2004, 2014 Christian Jaekl + +package net.jaekl.qd.xml; + + +public class MismatchedTagsException extends XmlParseException +{ + private static final long serialVersionUID = 1L; + + String m_expected; + String m_actual; + + public MismatchedTagsException(String expected, String actual) { + super(); + m_expected = expected; + m_actual = actual; + } + + public String getExpected() { return m_expected; } + public String getActual() { return m_actual; } +} diff --git a/prod/net/jaekl/qd/xml/MissingAttributeException.java b/prod/net/jaekl/qd/xml/MissingAttributeException.java new file mode 100644 index 0000000..d76fe00 --- /dev/null +++ b/prod/net/jaekl/qd/xml/MissingAttributeException.java @@ -0,0 +1,14 @@ +package net.jaekl.qd.xml; + +public class MissingAttributeException extends XmlParseException { + private static final long serialVersionUID = 1L; + + String m_tagName; + String m_attributeName; + + public MissingAttributeException(String tagName, String attributeName) + { + m_tagName = tagName; + m_attributeName = attributeName; + } +} diff --git a/prod/net/jaekl/qd/xml/MissingInfoException.java b/prod/net/jaekl/qd/xml/MissingInfoException.java new file mode 100644 index 0000000..1def37f --- /dev/null +++ b/prod/net/jaekl/qd/xml/MissingInfoException.java @@ -0,0 +1,47 @@ +// Copyright (C) 2004, 2014 Christian Jaekl + +package net.jaekl.qd.xml; + +import java.util.ArrayList; + +public class MissingInfoException extends XmlParseException +{ + private static final long serialVersionUID = 1L; + + String m_tagName; + ArrayList m_missingAttributes; + ArrayList m_missingChildTags; + + public MissingInfoException(String tagName) { + super(); + m_tagName = tagName; + m_missingAttributes = new ArrayList(); + m_missingChildTags = new ArrayList(); + } + + public void addMissingAttribute(String name) { + m_missingAttributes.add(name); + } + + public void addMissingChild(String name) { + m_missingChildTags.add(name); + } + + public String getTagName() { return m_tagName; } + + @Override + public String getMessage() { + StringBuilder sb = new StringBuilder(); + + sb.append("Tag: \"" + getTagName() + "\""); + + for (String attr : m_missingAttributes) { + sb.append("\n Attribute: \"" + attr + "\""); + } + + for (String child : m_missingChildTags) { + sb.append("\n Child tag: \"" + child + "\""); + } + return sb.toString(); + } +} diff --git a/prod/net/jaekl/qd/xml/ParseErrorHandler.java b/prod/net/jaekl/qd/xml/ParseErrorHandler.java new file mode 100644 index 0000000..ecdb780 --- /dev/null +++ b/prod/net/jaekl/qd/xml/ParseErrorHandler.java @@ -0,0 +1,29 @@ +// Copyright (C) 2014 Christian Jaekl + +// Simple SAX parse error handler. +// Necessary to avoid printing [Fatal Error] messages to stdout when something goes wrong. + +package net.jaekl.qd.xml; + +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +public class ParseErrorHandler implements ErrorHandler { + + @Override + public void error(SAXParseException saxpe) throws SAXException { + throw saxpe; + } + + @Override + public void fatalError(SAXParseException saxpe) throws SAXException { + throw saxpe; + } + + @Override + public void warning(SAXParseException saxpe) throws SAXException { + throw saxpe; + } + +} diff --git a/prod/net/jaekl/qd/xml/ParseHandler.java b/prod/net/jaekl/qd/xml/ParseHandler.java new file mode 100644 index 0000000..6bb2803 --- /dev/null +++ b/prod/net/jaekl/qd/xml/ParseHandler.java @@ -0,0 +1,123 @@ +// Copyright (C) 2004, 2015 Christian Jaekl + +package net.jaekl.qd.xml; + +import java.util.Stack; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; + +public class ParseHandler implements ContentHandler +{ + Stack m_stack; + + public ParseHandler(ParseResult root) { + m_stack = new Stack(); + m_stack.push(root); + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException + { + if (m_stack.isEmpty()) { + return; + } + + try { + m_stack.peek().characters(ch, start, length); + } + catch (XmlParseException xpe) { + throw new SAXException(xpe); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException + { + try { + if (m_stack.isEmpty()) { + return; + } + + boolean pop = m_stack.peek().endElement(uri, localName, qName); + if (pop) { + m_stack.pop(); + + if (m_stack.isEmpty()) { + return; + } + + m_stack.peek().endExternal(uri, localName, qName); + } + } + catch (XmlParseException xpe) { + throw new SAXException(xpe); + } + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException + { + try { + ParseResult current = m_stack.peek(); + ParseResult next = current.startElement(uri, localName, qName, attributes); + if (next != current) { + m_stack.push(next); + } + } + catch (XmlParseException xpe) { + throw new SAXException(xpe); + } + } + + @Override + public void endDocument() throws SAXException { + if (! m_stack.isEmpty()) { + String missingTag = m_stack.peek().getTagName(); + throw new SAXException(new MissingInfoException(missingTag)); + } + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + // no-op + } + + @Override + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException + { + // no-op + } + + @Override + public void processingInstruction(String target, String data) + throws SAXException + { + // no-op + } + + @Override + public void setDocumentLocator(Locator locator) { + // no-op + } + + @Override + public void skippedEntity(String name) throws SAXException { + // no-op + } + + @Override + public void startDocument() throws SAXException { + // no-op + } + + @Override + public void startPrefixMapping(String prefix, String uri) + throws SAXException + { + // no-op + } +} diff --git a/prod/net/jaekl/qd/xml/ParseResult.java b/prod/net/jaekl/qd/xml/ParseResult.java new file mode 100644 index 0000000..6c2c25f --- /dev/null +++ b/prod/net/jaekl/qd/xml/ParseResult.java @@ -0,0 +1,183 @@ +// Copyright (C) 2004, 2015 Christian Jaekl + +// Abstract class representing the result of parsing an XML Element. +// A class derived from this one will know how to parse a subtree inside an XML file, and +// will contain the result of that parse within itself when the parse has completed. +// +// Note that this code will need to be augmented and fixed if XML namespace support is desired. + +package net.jaekl.qd.xml; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Stack; + +import org.xml.sax.Attributes; +import org.xml.sax.helpers.AttributesImpl; + +public abstract class ParseResult +{ + Stack m_current; // Name of the element that we're currently inside + StringBuilder m_chars; // character content of m_current.peek() + ArrayList m_childParsers; // Set of all child parsers + boolean m_haveSeenMyTag; // Have I encountered my own (root) tag yet? + + String m_tagName; // Name of the (root) element tag that I'm parsing + HashSet m_internal; // Tags that we will store as members of this class instance + HashMap> m_external; // Tags that we will store as child ParseResult-derived objects + + // Information about the "current" tag, which we'll keep on the m_current stack. + static class CurrentInfo { + String m_tagName; + Attributes m_attr; + + public CurrentInfo(String tagName, Attributes attr) { + m_tagName = tagName; + m_attr = new AttributesImpl(attr); + } + + public String getTagName() { return m_tagName; } + public final Attributes getAttributes() { return m_attr; } + } + + @SuppressWarnings("unchecked") + public ParseResult(String tagName, String[] internalMemberTags, Object[][] externalParserTags) + { + m_current = new Stack(); + m_chars = new StringBuilder(); + m_childParsers = new ArrayList(); + m_haveSeenMyTag = false; + + m_tagName = tagName; + m_internal = new HashSet(); + m_external = new HashMap>(); + + for (String internalTag : internalMemberTags) { + m_internal.add(internalTag); + } + + for (int idx = 0; idx < externalParserTags.length; ++idx) { + String externalTag = (String)externalParserTags[idx][0]; + Class parserClass = (Class)externalParserTags[idx][1]; + m_external.put(externalTag, parserClass); + } + } + + public abstract void endContents(String uri, String localName, String qName, String chars, Attributes attr) throws XmlParseException; + public abstract void endExternal(String uri, String localName, String qName) throws XmlParseException; + + public String getTagName() { return m_tagName; } + public boolean haveSeenMyTag() { return m_haveSeenMyTag; } + + public void characters(char[] ch, int start, int length) throws XmlParseException + { + m_chars.append(ch, start, length); + } + + protected ParseResult[] collectParsedChildren(Class cls) { + ArrayList collection = new ArrayList(); + Iterator iter = m_childParsers.iterator(); + while (iter.hasNext()) { + ParseResult pr = iter.next(); + if (pr.getClass().isAssignableFrom(cls)) { + collection.add(pr); + iter.remove(); + } + } + + ParseResult[] result = new ParseResult[collection.size()]; + return collection.toArray(result); + } + + // returns true if this ParseResult's context has ended with this endElement() call + public boolean endElement(String uri, String localName, String qName) throws XmlParseException + { + assert (null != localName); + + boolean isInternal = m_internal.contains(localName); + + if (! m_haveSeenMyTag) { + // We're in some unrecognised prologue. Ignore it and move on. + return false; + } + + if (m_tagName.equals(localName)) { + validate(); + return true; + } + + if (!isInternal) { + // Unrecognized tag. Ignore it. + return false; + } + + CurrentInfo info = m_current.pop(); + String tag = info.getTagName(); + if ( ! tag.equals(localName) ) { + throw new MismatchedTagsException(tag, localName); + } + + String chars = m_chars.toString(); + endContents(uri, localName, qName, chars, info.getAttributes()); + + return false; + } + + // returns either itself, or a new ParseResult-derived object, whichever should handle parsing the inside of this element + public ParseResult startElement(String uri, String localName, String qName, Attributes attributes) + throws XmlParseException + { + assert (null != localName); + + m_chars.setLength(0); + + if (! m_haveSeenMyTag) { + // Have we opened our own (root) tag yet? + + if (m_tagName.equals(localName)) { + m_haveSeenMyTag = true; + return this; + } + else { + // One of two things has happened here. + // Either (a) we've got some sort of wrapper here, and have not yet reach our own tag, + // or (b) we're parsing XML that doesn't match expectations. + // In either case, we're going to ignore this tag, and scan forward looking for our own root. + return this; + } + } + + if (m_internal.contains(localName)) { + CurrentInfo info = new CurrentInfo(localName, attributes); + m_current.push(info); + return this; + } + + Class parserClass = m_external.get(localName); + if (null != parserClass) { + try { + ParseResult childParser = (ParseResult) parserClass.newInstance(); + m_childParsers.add(childParser); + return childParser.startElement(uri, localName, qName, attributes); + } + catch (IllegalAccessException iae) { + throw new XmlParseException(iae); + } + catch (InstantiationException ie) { + throw new XmlParseException(ie); + } + } + + // Not a recognized tag. Ignore it, rather than complaining. + return this; + } + + public void validate() throws XmlParseException + { + // Default implementation is a no-op. + // Override if you want to validate on endElement() + } +} + diff --git a/prod/net/jaekl/qd/xml/XmlParseException.java b/prod/net/jaekl/qd/xml/XmlParseException.java new file mode 100644 index 0000000..ada3fe5 --- /dev/null +++ b/prod/net/jaekl/qd/xml/XmlParseException.java @@ -0,0 +1,17 @@ +package net.jaekl.qd.xml; + +import net.jaekl.qd.QDException; + +public class XmlParseException extends QDException +{ + private static final long serialVersionUID = 1L; + + public XmlParseException() { + // no-op + } + + public XmlParseException(Throwable t) { + super(t); + } + +} diff --git a/test/net/jaekl/cfb/analyze/AnalyzerTest.java b/test/net/jaekl/cfb/analyze/AnalyzerTest.java new file mode 100644 index 0000000..d7e8395 --- /dev/null +++ b/test/net/jaekl/cfb/analyze/AnalyzerTest.java @@ -0,0 +1,36 @@ +package net.jaekl.cfb.analyze; + +import static org.junit.Assert.*; + +import java.io.File; + +import org.junit.Test; + +public class AnalyzerTest { + + @Test + public void testOutputWorkFile() { + final String[][] DATA = { + // findBugsDir, workDir, FBP, expectedOutputXml + { "foo", "bar", "baz.fbp", "bar" + File.separator + "baz.xml" }, + { + File.separator + "findbugs-3.01", + "." + File.separator + "work", + "project.fbp", + "." + File.separator + "work" + File.separator + "project.xml" + } + }; + + for (String[] datum : DATA) { + File findBugsDir = new File(datum[0]); + File workDir = new File(datum[1]); + File fbp = new File(datum[2]); + File expected = new File(datum[3]); + + Analyzer analyzer = new Analyzer(findBugsDir); + File actual = analyzer.outputWorkFile(workDir, fbp); + assertEquals(expected.getAbsolutePath(), actual.getAbsolutePath()); + } + } + +} diff --git a/test/net/jaekl/qd/xml/MissingInfoExceptionTest.java b/test/net/jaekl/qd/xml/MissingInfoExceptionTest.java new file mode 100644 index 0000000..8ce63bd --- /dev/null +++ b/test/net/jaekl/qd/xml/MissingInfoExceptionTest.java @@ -0,0 +1,40 @@ +package net.jaekl.qd.xml; + +import org.junit.Assert; + +import org.junit.Test; + +public class MissingInfoExceptionTest { + + @Test + public void test_getMessage_withSimpleTag() { + final String TAG = "TagNameGoesHere"; + MissingInfoException mie = new MissingInfoException(TAG); + String expected = "Tag: \"" + TAG + "\""; + String actual = mie.getMessage(); + Assert.assertTrue(actual.contains(expected)); + } + + @Test + public void test_getMessage_withAttributesAndChildren() { + final String AUGUSTUS = "Augustus"; + final String NOMEN = "nomen"; + final String COGNOMEN = "cognomen"; + final String TIBERIUS = "Tiberius"; + final String JULIA = "Julia"; + + MissingInfoException mie = new MissingInfoException(AUGUSTUS); + mie.addMissingAttribute(NOMEN); + mie.addMissingAttribute(COGNOMEN); + mie.addMissingChild(TIBERIUS); + mie.addMissingChild(JULIA); + + String actual = mie.getMessage(); + Assert.assertTrue(actual.contains("Tag: \"" + AUGUSTUS + "\"")); + Assert.assertTrue(actual.contains("Attribute: \"" + NOMEN + "\"")); + Assert.assertTrue(actual.contains("Attribute: \"" + COGNOMEN + "\"")); + Assert.assertTrue(actual.contains("Child tag: \"" + TIBERIUS + "\"")); + Assert.assertTrue(actual.contains("Child tag: \"" + JULIA + "\"")); + } + +} diff --git a/test/net/jaekl/qd/xml/ParseResultTest.java b/test/net/jaekl/qd/xml/ParseResultTest.java new file mode 100644 index 0000000..d58ab8c --- /dev/null +++ b/test/net/jaekl/qd/xml/ParseResultTest.java @@ -0,0 +1,524 @@ +// Copyright (C) 2004, 2015 Christian Jaekl + +package net.jaekl.qd.xml; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; + +import org.junit.Assert; + +import org.junit.Test; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; + +public class ParseResultTest { + // Some samples of XML that we're going to (try to) parse\ + private static final String MINIMAL_XML = + ""; + private static final String MINIMAL_XML_WITH_PROLOGUE = + ""; + private static final String XML_WITH_MINOR_CONTENT = + ""; + private static final String ROOT_INSIDE_SECONDARY_ELEMENT = + ""; + private static final String PROLOGUE_AND_SECONDARY_ELEMENT = + ""; + private static final String SIMPLE_INTERNAL_TAGS = + "content of two3"; + private static final String ROUTE_SUMMARY_FOR_STOP = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " 1234\n" + + " ONE-TWO-THREE-FOUR\n" + + " \n" + + " \n" + + " \n" + + " 123\n" + + " 0\n" + + " NORTH\n" + + " First Mall\n" + + " \n" + + " \n" + + " 123\n" + + " 1\n" + + " SOUTH\n" + + " Second Mall\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + private static final String NEXT_TRIPS_FOR_STOP = + "\n" + + "" + + "" + + "" + + "2438" + + "BRONSON SUNNYSIDE" + + "" + + "41Northbound" + + "Rideau C / Ctr Rideau" + + "" + + "Rideau Centre / Centre Rideau19:00" + + "160.45" + + "4LB - IN45.408957-75.664125" + + "66.4" + + "Rideau Centre / Centre Rideau" + + "19:3040-1" + + "4LB - IN" + + "Rideau Centre / Centre Rideau20:00" + + "70-1" + + "4LB - IN" + + "" + + ""; + + // Do the least possible parsing: check for the element only. + public static class MinimalParse extends ParseResult { + private static final String[] INTERNAL = {}; + private static final Object[][] EXTERNAL = {} ; + + public MinimalParse() { + super("Root", INTERNAL, EXTERNAL); + } + + @Override + public void endContents(String uri, String localName, String qName, + String chars, Attributes attr) throws XmlParseException + { + Assert.fail("Should not have any contents to end."); + } + + @Override + public void endExternal(String uri, String localName, String qName) + throws XmlParseException + { + Assert.fail("Should not have any external tags to end."); + } + } + + // Check that we can parse a minimal document without errors. + // Because there's no content being parsed (beyond the root element), there is + // no "correct" behaviour to assert. The test is to confirm that we + // don't do anything incorrect--no calls to endContent() nor endExternal(), + // and no exceptions thrown along the way. + @Test + public void test_withMinimalParse() throws IOException, SAXException { + MinimalParse mp = new MinimalParse(); + ByteArrayInputStream bais = null; + + String[] data = { + MINIMAL_XML, + MINIMAL_XML_WITH_PROLOGUE, + XML_WITH_MINOR_CONTENT, + ROOT_INSIDE_SECONDARY_ELEMENT + }; + + for (String datum : data) { + try { + bais = new ByteArrayInputStream(datum.getBytes("UTF-8")); + XMLReader reader = XMLReaderFactory.createXMLReader(); + ParseHandler ph = new ParseHandler(mp); + reader.setContentHandler(ph); + reader.parse(new InputSource(bais)); + } + finally { + if (null != bais) { + bais.close(); + } + } + } + } + + // If we parse something that doesn't have the expected root element, we should generate an exception + @Test + public void test_minimalParseWithMismatchedRootElement() throws IOException { + MinimalParse mp = new MinimalParse(); + ByteArrayInputStream bais = null; + + String[] data = { PROLOGUE_AND_SECONDARY_ELEMENT }; + + for (String datum : data) { + try { + bais = new ByteArrayInputStream(datum.getBytes("UTF-8")); + XMLReader reader = XMLReaderFactory.createXMLReader(); + ParseHandler ph = new ParseHandler(mp); + reader.setContentHandler(ph); + reader.parse(new InputSource(bais)); + Assert.fail("Should have thrown an exception."); + } + catch ( SAXException se ) { + Throwable cause = se.getCause(); + Assert.assertNotNull(cause); + Assert.assertTrue(cause instanceof MissingInfoException); + MissingInfoException mie = (MissingInfoException) cause; + Assert.assertEquals("Root", mie.getTagName()); + } + finally { + if (null != bais) { + bais.close(); + } + } + } + } + + // Do the some simple parsing: and some subtags that are processed internally + public static class SimpleParse extends ParseResult { + private static final String ONE = "One"; + private static final String TWO = "Two"; + private static final String THREE = "Three"; + + private static final String[] INTERNAL = {ONE, TWO, THREE}; + private static final Object[][] EXTERNAL = {} ; + + String m_one; + String m_two; + String m_three; + + public SimpleParse() { + super("Root", INTERNAL, EXTERNAL); + + m_one = m_two = m_three = null; + } + + public String getOne() { return m_one; } + public String getTwo() { return m_two; } + public String getThree() { return m_three; } + + @Override + public void endContents(String uri, String localName, String qName, + String chars, Attributes attr) throws XmlParseException + { + if (localName.equals(ONE)) { + m_one = chars; + } + else if (localName.equals(TWO)) { + m_two = chars; + } + else if (localName.equals(THREE)) { + m_three = chars; + } + } + + @Override + public void endExternal(String uri, String localName, String qName) + throws XmlParseException + { + Assert.fail("Should not have any external tags to end."); + } + } + + // Parse some XML containing subtags that are handled internally by SimpleParse + @Test + public void test_parseWithInternalSubtags() throws IOException, SAXException + { + SimpleParse sp = new SimpleParse(); + ByteArrayInputStream bais = null; + + String[] data = { + SIMPLE_INTERNAL_TAGS + }; + + for (String datum : data) { + try { + bais = new ByteArrayInputStream(datum.getBytes("UTF-8")); + XMLReader reader = XMLReaderFactory.createXMLReader(); + ParseHandler ph = new ParseHandler(sp); + reader.setContentHandler(ph); + reader.parse(new InputSource(bais)); + + Assert.assertEquals("", sp.getOne()); + Assert.assertEquals("content of two", sp.getTwo()); + Assert.assertEquals("3", sp.getThree()); + } + finally { + if (null != bais) { + bais.close(); + } + } + } + } + + // Parse sub-tags, handling some internally and some externally + public static class RouteSummaryParse extends ParseResult { + private static final String STOP_NO = "StopNo"; + private static final String STOP_DESCR = "StopDescription"; + private static final String ERROR = "Error"; + private static final String ROUTES = "Routes"; + private static final String ROUTE = "Route"; + + private static final String[] INTERNAL = {STOP_NO, STOP_DESCR, ERROR, ROUTES}; + private static final Object[][] EXTERNAL = { {ROUTE, RouteParse.class} }; + + // Data gleaned from parsing + int m_stopNo; + String m_stopDescr; + String m_error; + ArrayList m_routes; + + public RouteSummaryParse() { + super("GetRouteSummaryForStopResult", INTERNAL, EXTERNAL); + + m_stopNo = 0; + m_stopDescr = m_error = null; + m_routes = new ArrayList(); + } + + public int getStopNo() { return m_stopNo; } + public String getStopDescription() { return m_stopDescr; } + public String getError() { return m_error; } + public int getNumRoutes() { return m_routes.size(); } + public RouteParse getRoute(int idx) { return m_routes.get(idx); } + + @Override + public void endContents(String uri, String localName, String qName, + String chars, Attributes attr) throws XmlParseException + { + if (localName.equals(STOP_NO)) { + m_stopNo = Integer.parseInt(chars); + } + else if (localName.equals(STOP_DESCR)) { + m_stopDescr = chars; + } + else if (localName.equals(ERROR)) { + m_error = chars; + } + } + + @Override + public void endExternal(String uri, String localName, String qName) + throws XmlParseException + { + if (localName.equals(ROUTE)) { + ParseResult[] collected = collectParsedChildren(RouteParse.class); + for (ParseResult pr : collected) { + Assert.assertTrue(pr instanceof RouteParse); + m_routes.add((RouteParse)pr); + } + } + } + } + public static class RouteParse extends ParseResult { + private static final String ROUTE = "Route"; + private static final String ROUTE_NO = "RouteNo"; + private static final String DIR_ID = "DirectionID"; + private static final String DIR = "Direction"; + private static final String HEADING = "RouteHeading"; + private static final String TRIPS = "Trips"; + private static final String TRIP = "Trip"; + + private static final String[] INTERNAL = {ROUTE_NO, DIR_ID, DIR, HEADING, TRIPS}; + private static final Object[][] EXTERNAL = { {TRIP, TripParse.class} }; + + // Data gleaned from parsing + int m_routeNo; + int m_dirID; + String m_dir; + String m_heading; + ArrayList m_trips; + + public RouteParse() { + super(ROUTE, INTERNAL, EXTERNAL); + + m_routeNo = m_dirID = 0; + m_dir = m_heading = null; + m_trips = new ArrayList(); + } + + public int getRouteNo() { return m_routeNo; } + public int getDirectionID() { return m_dirID; } + public String getDirection() { return m_dir; } + public String getHeading() { return m_heading; } + public int getNumTrips() { return m_trips.size(); } + public TripParse getTrip(int idx) { return m_trips.get(idx); } + + @Override + public void endContents(String uri, String localName, String qName, + String chars, Attributes attr) throws XmlParseException + { + if (localName.equals(ROUTE_NO)) { + m_routeNo = Integer.parseInt(chars); + } + else if (localName.equals(DIR_ID)) { + m_dirID = Integer.parseInt(chars); + } + else if (localName.equals(DIR)) { + m_dir = chars; + } + else if (localName.equals(HEADING)) { + m_heading = chars; + } + } + + @Override + public void endExternal(String uri, String localName, String qName) + throws XmlParseException + { + if (localName.equals(TRIP)) { + ParseResult[] collected = collectParsedChildren(TripParse.class); + for (ParseResult pr : collected) { + Assert.assertTrue(pr instanceof TripParse); + m_trips.add((TripParse)pr); + } + } + + } + } + public static class TripParse extends ParseResult { + private static final String TRIP = "Trip"; + private static final String TRIP_DEST = "TripDestination"; + private static final String TRIP_START = "TripStartTime"; + private static final String ADJ_SCHED_TIME = "AdjustedScheduleTime"; + + private static final String[] INTERNAL = {TRIP_DEST, TRIP_START, ADJ_SCHED_TIME }; + private static final Object[][] EXTERNAL = { }; + + // Data gleaned from parsing + String m_dest; + String m_startTime; + int m_adjSchedTime;; + + public TripParse() { + super(TRIP, INTERNAL, EXTERNAL); + + m_dest = m_startTime = null; + m_adjSchedTime = 0; + } + + public String getDestination() { return m_dest; } + public String getStartTime() { return m_startTime; } + public int getAdjustedScheduleTime() { return m_adjSchedTime; } + + @Override + public void endContents(String uri, String localName, String qName, + String chars, Attributes attr) throws XmlParseException + { + if (localName.equals(TRIP_DEST)) { + m_dest = chars; + } + else if (localName.equals(TRIP_START)) { + m_startTime = chars; + } + else if (localName.equals(ADJ_SCHED_TIME)) { + m_adjSchedTime = Integer.parseInt(chars); + } + } + + @Override + public void endExternal(String uri, String localName, String qName) + throws XmlParseException + { + Assert.fail("Should not be attempting to parse external tags."); + } + } + + // Parse some XML containing subtags that are handled both internally and externally + @Test + public void test_parseRouteSummary() throws IOException, SAXException + { + RouteSummaryParse rsp = new RouteSummaryParse(); + ByteArrayInputStream bais = null; + + try { + RouteParse rp; + + bais = new ByteArrayInputStream(ROUTE_SUMMARY_FOR_STOP.getBytes("UTF-8")); + XMLReader reader = XMLReaderFactory.createXMLReader(); + ParseHandler ph = new ParseHandler(rsp); + reader.setContentHandler(ph); + reader.parse(new InputSource(bais)); + + Assert.assertEquals(1234, rsp.getStopNo()); + Assert.assertEquals("ONE-TWO-THREE-FOUR", rsp.getStopDescription()); + Assert.assertEquals("", rsp.getError()); + + Assert.assertEquals(2, rsp.getNumRoutes()); + + rp = rsp.getRoute(0); + Assert.assertNotNull(rp); + Assert.assertEquals(123, rp.getRouteNo()); + Assert.assertEquals(0, rp.getDirectionID()); + Assert.assertEquals("NORTH", rp.getDirection()); + Assert.assertEquals("First Mall", rp.getHeading()); + + rp = rsp.getRoute(1); + Assert.assertNotNull(rp); + Assert.assertEquals(123, rp.getRouteNo()); + Assert.assertEquals(1, rp.getDirectionID()); + Assert.assertEquals("SOUTH", rp.getDirection()); + Assert.assertEquals("Second Mall", rp.getHeading()); + } + finally { + if (null != bais) { + bais.close(); + } + } + } + + // Parse a 3-level external-tag hierarchy: RouteSummary contains Routes contains Trips + @Test + public void test_parseThreeLevels() throws IOException, SAXException + { + RouteSummaryParse rsp = new RouteSummaryParse(); + ByteArrayInputStream bais = null; + + try { + RouteParse rp; + TripParse tp; + + bais = new ByteArrayInputStream(NEXT_TRIPS_FOR_STOP.getBytes("UTF-8")); + XMLReader reader = XMLReaderFactory.createXMLReader(); + ParseHandler ph = new ParseHandler(rsp); + reader.setContentHandler(ph); + reader.parse(new InputSource(bais)); + + Assert.assertEquals(2438, rsp.getStopNo()); + Assert.assertEquals("BRONSON SUNNYSIDE", rsp.getStopDescription()); + Assert.assertEquals("", rsp.getError()); + + Assert.assertEquals(1, rsp.getNumRoutes()); + + rp = rsp.getRoute(0); + Assert.assertNotNull(rp); + Assert.assertEquals(4, rp.getRouteNo()); + Assert.assertEquals(1, rp.getDirectionID()); + Assert.assertEquals("Northbound", rp.getDirection()); + Assert.assertEquals("Rideau C / Ctr Rideau", rp.getHeading()); + + Assert.assertEquals(3, rp.getNumTrips()); + + tp = rp.getTrip(0); + Assert.assertNotNull(tp); + Assert.assertEquals("Rideau Centre / Centre Rideau", tp.getDestination()); + Assert.assertEquals("19:00", tp.getStartTime()); + Assert.assertEquals(16, tp.getAdjustedScheduleTime()); + + tp = rp.getTrip(1); + Assert.assertNotNull(tp); + Assert.assertEquals("Rideau Centre / Centre Rideau", tp.getDestination()); + Assert.assertEquals("19:30", tp.getStartTime()); + Assert.assertEquals(40, tp.getAdjustedScheduleTime()); + + tp = rp.getTrip(2); + Assert.assertNotNull(tp); + Assert.assertEquals("Rideau Centre / Centre Rideau", tp.getDestination()); + Assert.assertEquals("20:00", tp.getStartTime()); + Assert.assertEquals(70, tp.getAdjustedScheduleTime()); + } + finally { + if (null != bais) { + bais.close(); + } + } + } +} -- 2.39.2