1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * 
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * The contents of this file are subject to the terms of either the Universal Permissive License
   7  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   8  *
   9  * or the following license:
  10  *
  11  * Redistribution and use in source and binary forms, with or without modification, are permitted
  12  * provided that the following conditions are met:
  13  * 
  14  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  15  * and the following disclaimer.
  16  * 
  17  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  18  * conditions and the following disclaimer in the documentation and/or other materials provided with
  19  * the distribution.
  20  * 
  21  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  22  * endorse or promote products derived from this software without specific prior written permission.
  23  * 
  24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  25  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  26  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  27  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  30  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  31  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  32  */
  33 package org.openjdk.jmc.flightrecorder.test.rules.jdk;
  34 
  35 import java.io.File;
  36 import java.io.FileNotFoundException;
  37 import java.io.FileOutputStream;
  38 import java.io.IOException;
  39 import java.io.OutputStream;
  40 import java.util.ArrayDeque;
  41 import java.util.ArrayList;
  42 import java.util.Collection;
  43 import java.util.Deque;
  44 import java.util.Iterator;
  45 import java.util.List;
  46 import java.util.Objects;
  47 import java.util.SortedMap;
  48 import java.util.TimeZone;
  49 import java.util.TreeMap;
  50 import java.util.concurrent.ExecutionException;
  51 import java.util.concurrent.RunnableFuture;
  52 
  53 import javax.xml.parsers.DocumentBuilder;
  54 import javax.xml.parsers.DocumentBuilderFactory;
  55 import javax.xml.parsers.ParserConfigurationException;
  56 import javax.xml.transform.OutputKeys;
  57 import javax.xml.transform.Transformer;
  58 import javax.xml.transform.TransformerException;
  59 import javax.xml.transform.TransformerFactory;
  60 import javax.xml.transform.dom.DOMSource;
  61 import javax.xml.transform.stream.StreamResult;
  62 import javax.xml.xpath.XPath;
  63 import javax.xml.xpath.XPathConstants;
  64 import javax.xml.xpath.XPathExpression;
  65 import javax.xml.xpath.XPathExpressionException;
  66 import javax.xml.xpath.XPathFactory;
  67 
  68 import org.junit.After;
  69 import org.junit.Assert;
  70 import org.junit.Before;
  71 import org.junit.Test;
  72 import org.junit.experimental.categories.Category;
  73 import org.openjdk.jmc.common.item.IAttribute;
  74 import org.openjdk.jmc.common.item.IItem;
  75 import org.openjdk.jmc.common.item.IItemCollection;
  76 import org.openjdk.jmc.common.item.IItemIterable;
  77 import org.openjdk.jmc.common.item.IItemQuery;
  78 import org.openjdk.jmc.common.item.IMemberAccessor;
  79 import org.openjdk.jmc.common.item.IType;
  80 import org.openjdk.jmc.common.test.SlowTests;
  81 import org.openjdk.jmc.common.test.TestToolkit;
  82 import org.openjdk.jmc.common.test.io.IOResource;
  83 import org.openjdk.jmc.common.test.io.IOResourceSet;
  84 import org.openjdk.jmc.common.util.IPreferenceValueProvider;
  85 import org.openjdk.jmc.flightrecorder.CouldNotLoadRecordingException;
  86 import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit;
  87 import org.openjdk.jmc.flightrecorder.rules.IRule;
  88 import org.openjdk.jmc.flightrecorder.rules.Result;
  89 import org.openjdk.jmc.flightrecorder.rules.RuleRegistry;
  90 import org.openjdk.jmc.flightrecorder.rules.Severity;
  91 import org.w3c.dom.Document;
  92 import org.w3c.dom.Element;
  93 import org.w3c.dom.Node;
  94 import org.w3c.dom.NodeList;
  95 import org.xml.sax.SAXException;
  96 
  97 /**
  98  * Class for testing jfr rule report consistency
  99  */
 100 @SuppressWarnings("nls")
 101 public class TestRulesWithJfr {
 102         private static final String JFR_RULE_BASELINE_JFR = "JfrRuleBaseline.xml";
 103         private static final String BASELINE_DIR = "baseline";
 104         private static final String RECORDINGS_DIR = "jfr";
 105         private static final String RECORDINGS_INDEXFILE = "index.txt";
 106 
 107         private TimeZone defaultTimeZone;
 108         
 109         @Before
 110         public void before() {
 111                 // empty the log before each test
 112                 DetailsTracker.clear();
 113                 // force UTC time zone during test
 114                 defaultTimeZone = TimeZone.getDefault();
 115                 TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
 116         }
 117         
 118         @After
 119         public void after() {
 120                 // restore previous default time zone
 121                 TimeZone.setDefault(defaultTimeZone);
 122         }
 123 
 124         @Test
 125         public void verifyOneResult() throws IOException {
 126                 verifyRuleResults(true);
 127         }
 128 
 129         @Category(value = {SlowTests.class})
 130         @Test
 131         public void verifyAllResults() throws IOException {
 132                 verifyRuleResults(false);
 133         }
 134 
 135         private void verifyRuleResults(boolean onlyOneRecording) throws IOException {
 136                 IOResourceSet jfrs = TestToolkit.getResourcesInDirectory(TestRulesWithJfr.class, RECORDINGS_DIR, RECORDINGS_INDEXFILE);
 137                 String reportName = null;
 138                 if (onlyOneRecording) {
 139                         IOResource firstJfr = jfrs.iterator().next();
 140                         jfrs = new IOResourceSet(firstJfr);
 141                         reportName = firstJfr.getName();
 142                 }
 143                 // Run all the .jfr files in the directory through the rule engine
 144                 ReportCollection rulesReport = generateRulesReport(jfrs);
 145 
 146                 // Parse the baseline XML file
 147                 ReportCollection baselineReport = parseRulesReportXml(BASELINE_DIR, JFR_RULE_BASELINE_JFR, reportName);
 148 
 149                 // Compare the baseline with the current rule results
 150                 boolean resultsEqual = rulesReport.compareAndLog(baselineReport);
 151 
 152                 // Save file for later inspection and/or updating the baseline with
 153                 if (!resultsEqual) {
 154                         // Save the generated file to XML
 155                         saveToFile(rulesReport.toXml(), BASELINE_DIR, JFR_RULE_BASELINE_JFR, onlyOneRecording);
 156                 }
 157 
 158                 // Assert that the comparison returned true
 159                 Assert.assertTrue(DetailsTracker.getEntries(), resultsEqual);
 160         }
 161 
 162         private static void saveToFile(Document doc, String directory, String fileName, boolean onlyOneRecording) {
 163                 String filePath = getResultDir().getAbsolutePath() + File.separator
 164                                 + ((directory != null) ? (directory + File.separator) : "")
 165                                 + (onlyOneRecording ? "Generated_One_" : "Generated_") + fileName;
 166                 File resultFile = new File(filePath);
 167                 prepareFile(resultFile);
 168                 try {
 169                         writeDomToStream(doc, new FileOutputStream(resultFile));
 170                 } catch (FileNotFoundException e) {
 171                         e.printStackTrace();
 172                 }
 173         }
 174 
 175         private static void prepareFile(File file) {
 176                 if (file.exists()) {
 177                         file.delete();
 178                 }
 179                 File parent = file.getParentFile();
 180                 if (parent != null) {
 181                         parent.mkdirs();
 182                 }
 183                 try {
 184                         file.createNewFile();
 185                 } catch (IOException e) {
 186                         e.printStackTrace();
 187                         Assert.fail("Error creating file \"" + file.getAbsolutePath() + "\". Error:\n" + e.getMessage());
 188                 }
 189         }
 190 
 191         private static void writeDomToStream(Document doc, OutputStream os) {
 192                 try {
 193                         TransformerFactory transformerFactory = TransformerFactory.newInstance();
 194                         Transformer transformer = transformerFactory.newTransformer();
 195                         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
 196                         DOMSource source = new DOMSource(doc);
 197                         StreamResult console = new StreamResult(os);
 198                         transformer.transform(source, console);
 199                 } catch (TransformerException e) {
 200                         e.printStackTrace();
 201                 }
 202         }
 203 
 204         private static ReportCollection parseRulesReportXml(String directory, String fileName, String reportName) {
 205                 ReportCollection collection = new ReportCollection();
 206                 try {
 207                         // FIXME: No need to go via temp file. Just get the input stream directly from the resource.
 208                         File dir = TestToolkit.materialize(TestRulesWithJfr.class, directory, fileName);
 209                         File file = new File(dir, fileName);
 210                         DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
 211                         DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
 212                         Document baselineDoc = docBuilder.parse(file);
 213                         collection = ReportCollection.fromXml(baselineDoc, reportName);
 214                 } catch (ParserConfigurationException | SAXException | IOException e) {
 215                         e.printStackTrace();
 216                 }
 217                 return collection;
 218         }
 219 
 220         private static ReportCollection generateRulesReport(IOResourceSet jfrs) {
 221                 ReportCollection collection = new ReportCollection();
 222                 for (IOResource jfr : jfrs) {
 223                         Report report = generateReport(jfr, false, null);
 224                         collection.put(report.getName(), report);
 225                 }
 226                 return collection;
 227         }
 228 
 229         private static File getResultDir() {
 230                 if (System.getProperty("results.dir") != null) {
 231                         return new File(System.getProperty("results.dir"));
 232                 } else {
 233                         return new File(System.getProperty("user.dir"));
 234                 }
 235         }
 236 
 237         private static Report generateReport(IOResource jfr, boolean verbose, Severity minSeverity) {
 238                 Report report = new Report(jfr.getName());
 239                 try {
 240                         IItemCollection events = JfrLoaderToolkit.loadEvents(jfr.open());
 241 
 242                         for (IRule rule : RuleRegistry.getRules()) {
 243                                 try {
 244                                         RunnableFuture<Result> future = rule.evaluate(events,
 245                                                         IPreferenceValueProvider.DEFAULT_VALUES);
 246                                         future.run();
 247                                         Result result = future.get();
 248 //                                      for (Result result : results) {
 249                                         if (minSeverity == null || Severity.get(result.getScore()).compareTo(minSeverity) >= 0) {
 250                                                 ItemSet itemSet = null;
 251                                                 IItemQuery itemQuery = result.getItemQuery();
 252                                                 if (verbose && itemQuery != null && !itemQuery.getAttributes().isEmpty()) {
 253                                                         itemSet = new ItemSet();
 254                                                         IItemCollection resultEvents = events.apply(itemQuery.getFilter());
 255                                                         Collection<? extends IAttribute<?>> attributes = itemQuery.getAttributes();
 256                                                         for (IAttribute<?> attribute : attributes) {
 257                                                                 itemSet.addField(attribute.getName());
 258                                                         }
 259                                                         Iterator<? extends IItemIterable> iterables = resultEvents.iterator();
 260                                                         while (iterables.hasNext()) {
 261                                                                 IItemIterable ii = iterables.next();
 262                                                                 IType<IItem> type = ii.getType();
 263                                                                 List<IMemberAccessor<?, IItem>> accessors = new ArrayList<>(attributes.size());
 264                                                                 for (IAttribute<?> a : attributes) {
 265                                                                         accessors.add(a.getAccessor(type));
 266                                                                 }
 267                                                                 Iterator<? extends IItem> items = ii.iterator();
 268                                                                 while (items.hasNext()) {
 269                                                                         ItemList itemList = new ItemList();
 270                                                                         IItem item = items.next();
 271                                                                         for (IMemberAccessor<?, IItem> a : accessors) {
 272                                                                                 itemList.add(String.valueOf(a.getMember(item)));
 273                                                                         }
 274                                                                         itemSet.addItem(itemList);
 275                                                                 }
 276                                                         }
 277                                                 }
 278                                                 RuleResult ruleResult = new RuleResult(String.valueOf(result.getRule().getId()),
 279                                                                 Severity.get(result.getScore()).getLocalizedName(), String.valueOf(result.getScore()),
 280                                                                 result.getShortDescription(), result.getLongDescription(), itemSet);
 281                                                 report.put(String.valueOf(result.getRule().getId()), ruleResult);
 282 //                                              }
 283                                         }
 284                                 } catch (RuntimeException | InterruptedException | ExecutionException e) {
 285                                         System.out.println("Problem while evaluating rules for \"" + jfr.getName() + "\". Message: "
 286                                                         + e.getLocalizedMessage());
 287                                 }
 288                         }
 289                 } catch (IOException | CouldNotLoadRecordingException e) {
 290                         e.printStackTrace();
 291                 }
 292                 return report;
 293         }
 294 
 295         private static Element createValueNode(Document doc, String name, String value) {
 296                 Element node = doc.createElement(name);
 297                 node.appendChild(doc.createTextNode(value != null ? value : ""));
 298                 return node;
 299         }
 300 
 301         private static List<String> getNodeValues(String xpathExpr, Node node) {
 302                 List<String> values = new ArrayList<>();
 303                 try {
 304                         XPath xpath = XPathFactory.newInstance().newXPath();
 305                         XPathExpression expression = xpath.compile(xpathExpr);
 306                         NodeList nodes = ((NodeList) expression.evaluate(node, XPathConstants.NODESET));
 307                         for (int i = 0; i < nodes.getLength(); i++) {
 308                                 Node thisNodeOnly = nodes.item(i);
 309                                 thisNodeOnly.getParentNode().removeChild(thisNodeOnly);
 310                                 Node child = thisNodeOnly.getFirstChild();
 311                                 if (child != null) {
 312                                         values.add(child.getNodeValue());
 313                                 } else {
 314                                         values.add("");
 315                                 }
 316                         }
 317                 } catch (XPathExpressionException e) {
 318                         e.printStackTrace();
 319                 }
 320                 return values;
 321         }
 322 
 323         private static NodeList getNodeSet(String expr, Node node) {
 324                 NodeList result = null;
 325                 try {
 326                         XPath xpath = XPathFactory.newInstance().newXPath();
 327                         XPathExpression xPath = xpath.compile(expr);
 328                         result = (NodeList) xPath.evaluate(node, XPathConstants.NODESET);
 329                 } catch (XPathExpressionException e) {
 330                         e.printStackTrace();
 331                 }
 332                 return result;
 333         }
 334 
 335         private static class ReportCollection {
 336                 private SortedMap<String, Report> reports;
 337 
 338                 public ReportCollection() {
 339                         reports = new TreeMap<>();
 340                 }
 341 
 342                 public void put(String filename, Report report) {
 343                         reports.put(filename, report);
 344                 }
 345 
 346                 public Report get(String filename) {
 347                         return reports.get(filename);
 348                 }
 349 
 350                 public boolean compareAndLog(Object other) {
 351                         ReportCollection otherReportCollection = (ReportCollection) other;
 352                         boolean equals = reports.size() == otherReportCollection.reports.size();
 353                         if (!equals) {
 354                                 if (reports.size() > otherReportCollection.reports.size()) {
 355                                         for (String reportname : reports.keySet()) {
 356                                                 if (otherReportCollection.get(reportname) == null) {
 357                                                         DetailsTracker.log("Report for " + reportname
 358                                                                         + " could not be found in the other report collection. ");
 359                                                 }
 360                                         }
 361                                 } else {
 362                                         for (String reportname : otherReportCollection.reports.keySet()) {
 363                                                 if (reports.get(reportname) == null) {
 364                                                         DetailsTracker.log(
 365                                                                         "Report for " + reportname + " could not be found in this report collection. ");
 366                                                 }
 367                                         }
 368                                 }
 369                                 DetailsTracker.log("\n");
 370                         }
 371                         for (String reportname : reports.keySet()) {
 372                                 Report otherReport = otherReportCollection.get(reportname);
 373                                 if (otherReport != null) {
 374                                         equals = reports.get(reportname).compareAndLog(otherReport) && equals;
 375                                 } else {
 376                                         DetailsTracker
 377                                                         .log("\nReport for " + reportname + " could not be found in the other report collection. ");
 378                                         equals = false;
 379                                 }
 380                         }
 381                         return equals;
 382                 }
 383 
 384                 public Document toXml() {
 385                         DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
 386                         DocumentBuilder docBuilder;
 387                         Document doc = null;
 388                         try {
 389                                 docBuilder = docFactory.newDocumentBuilder();
 390                                 doc = docBuilder.newDocument();
 391                                 Element rootElement = doc.createElement("reportcollection");
 392                                 doc.appendChild(rootElement);
 393                                 for (Report report : reports.values()) {
 394                                         report.toXml(rootElement);
 395                                 }
 396                         } catch (ParserConfigurationException e) {
 397                                 e.printStackTrace();
 398                         }
 399                         return doc;
 400                 }
 401 
 402                 public static ReportCollection fromXml(Document doc, String reportName) {
 403                         ReportCollection collection = new ReportCollection();
 404                         NodeList reports = getNodeSet("//report", doc);
 405                         for (int i = 0; i < reports.getLength(); i++) {
 406                                 Node thisReportOnly = reports.item(i);
 407                                 thisReportOnly.getParentNode().removeChild(thisReportOnly);
 408                                 Report report = Report.fromXml(thisReportOnly);
 409                                 if (reportName == null || report.getName().equals(reportName)) {
 410                                         collection.put(report.getName(), report);
 411                                 }
 412                         }
 413                         return collection;
 414                 }
 415         }
 416 
 417         private static class Report {
 418                 private String filename;
 419                 private SortedMap<String, RuleResult> rules;
 420 
 421                 public Report(String filename) {
 422                         this.filename = filename;
 423                         rules = new TreeMap<>();
 424                 }
 425 
 426                 public void put(String id, RuleResult rule) {
 427                         rules.put(id, rule);
 428                 }
 429 
 430                 public RuleResult get(String id) {
 431                         return rules.get(id);
 432                 }
 433 
 434                 public String getName() {
 435                         return filename;
 436                 }
 437 
 438                 public boolean compareAndLog(Object other) {
 439                         Report otherReport = (Report) other;
 440                         boolean equals = rules.size() == otherReport.rules.size();
 441                         boolean fileNamePrinted = false;
 442                         if (equals) {
 443                                 for (String rulename : rules.keySet()) {
 444                                         RuleResult otherRule = otherReport.get(rulename);
 445                                         if (otherRule != null) {
 446                                                 equals = rules.get(rulename).compareAndLog(otherRule) && equals;
 447                                                 if (!equals && !fileNamePrinted) {
 448                                                         DetailsTracker.log("\n\nReport: \"" + filename + "\", ");
 449                                                         fileNamePrinted = true;
 450                                                 }
 451                                         } else {
 452                                                 DetailsTracker.log("\n\nReport: \"" + filename + "\". Rule result for " + rulename
 453                                                                 + " could not be found in the other report. ");
 454                                                 equals = false;
 455                                         }
 456                                 }
 457                         } else {
 458                                 if (rules.size() > otherReport.rules.size()) {
 459                                         for (String ruleId : rules.keySet()) {
 460                                                 RuleResult otherRule = otherReport.get(ruleId);
 461                                                 if (otherRule != null) {
 462                                                         equals = rules.get(ruleId).compareAndLog(otherRule) && equals;
 463                                                 } else {
 464                                                         DetailsTracker.log("\nReport for file \"" + filename + "\", rule result for \"" + ruleId
 465                                                                         + "\" could not be found in the other report. ");
 466                                                 }
 467                                         }
 468                                 } else {
 469                                         for (String ruleId : otherReport.rules.keySet()) {
 470                                                 RuleResult rule = rules.get(ruleId);
 471                                                 if (rule != null) {
 472                                                         equals = rule.compareAndLog(otherReport.rules.get(ruleId)) && equals;
 473                                                 } else {
 474                                                         DetailsTracker.log("\nReport for file \"" + filename + "\", rule result for \"" + ruleId
 475                                                                         + "\" could not be found in this report. ");
 476                                                 }
 477                                         }
 478                                 }
 479                                 DetailsTracker.log("\n");
 480                         }
 481                         return equals;
 482                 }
 483 
 484                 public void toXml(Element parent) {
 485                         Element reportNode = parent.getOwnerDocument().createElement("report");
 486                         parent.appendChild(reportNode);
 487                         reportNode.appendChild(createValueNode(parent.getOwnerDocument(), "file", filename));
 488                         for (RuleResult rule : rules.values()) {
 489                                 rule.toXml(reportNode);
 490                         }
 491                 }
 492 
 493                 public static Report fromXml(Node node) {
 494                         Report report = new Report(getNodeValues("./file", node).get(0));
 495                         NodeList rules = getNodeSet("./rule", node);
 496                         for (int i = 0; i < rules.getLength(); i++) {
 497                                 Node thisRuleOnly = rules.item(i);
 498                                 thisRuleOnly.getParentNode().removeChild(thisRuleOnly);
 499                                 RuleResult rule = RuleResult.fromXml(thisRuleOnly);
 500                                 report.put(rule.getId(), rule);
 501                         }
 502                         return report;
 503                 }
 504         }
 505 
 506         private static class RuleResult {
 507                 private String id;
 508                 private String severity;
 509                 private String score;
 510                 private String shortDescription;
 511                 private String longDescription;
 512                 private ItemSet itemset;
 513 
 514                 public RuleResult(String id, String severity, String score, String shortDescription, String longDescription,
 515                                 ItemSet itemset) {
 516                         this.id = id;
 517                         this.severity = severity;
 518                         this.score = score;
 519                         this.shortDescription = shortDescription;
 520                         this.longDescription = longDescription;
 521                         this.itemset = itemset;
 522                 }
 523 
 524                 public String getId() {
 525                         return id;
 526                 }
 527 
 528                 public boolean compareAndLog(Object other) {
 529                         RuleResult otherRule = (RuleResult) other;
 530                         boolean scoreEquals = Objects.equals(score, otherRule.score);
 531                         if (!scoreEquals) {
 532                                 // determine if this is just a rounding error
 533                                 scoreEquals = (Math.abs(Float.valueOf(score) - Float.valueOf(otherRule.score)) < 0.0000000000001f) ? true
 534                                                 : false;
 535                                 if (scoreEquals) {
 536                                         // apparently a rounding issue. Print it out for informational purposes
 537                                         System.out
 538                                                         .println("Rule \"" + id + "\": Encountered rounding issue for score when comparing values "
 539                                                                         + score + " and " + otherRule.score);
 540                                 }
 541                         }
 542                         boolean itemSetEquality = compareAndLogItemSets(other);
 543                         boolean ruleEquality = Objects.equals(severity, otherRule.severity) && scoreEquals
 544                                         && Objects.equals(shortDescription, otherRule.shortDescription)
 545                                         && Objects.equals(longDescription, otherRule.longDescription);
 546                         if (!ruleEquality) {
 547                                 if (!Objects.equals(severity, otherRule.severity)) {
 548                                         DetailsTracker.log("\n    Severity mismatch: \"" + severity + "\" was not equal to \""
 549                                                         + otherRule.severity + "\". ");
 550                                 }
 551                                 if (!scoreEquals) {
 552                                         DetailsTracker.log(
 553                                                         "\n    Score mismatch: \"" + score + "\" was not equal to \"" + otherRule.score + "\". ");
 554                                 }
 555                                 if (!Objects.equals(shortDescription, otherRule.shortDescription)) {
 556                                         DetailsTracker.log("\n    Message mismatch: \"" + shortDescription + "\" was not equal to \""
 557                                                         + otherRule.shortDescription + "\". ");
 558                                 }
 559                                 if (!Objects.equals(longDescription, otherRule.longDescription)) {
 560                                         DetailsTracker.log("\n    Description mismatch: \"" + longDescription + "\" was not equal to \""
 561                                                         + otherRule.longDescription + "\". ");
 562                                 }
 563                         }
 564                         if (!(itemSetEquality && ruleEquality)) {
 565                                 DetailsTracker.log("\n  Rule: \"" + id + "\". ");
 566                         }
 567                         return itemSetEquality && ruleEquality;
 568                 }
 569 
 570                 private boolean compareAndLogItemSets(Object other) {
 571                         RuleResult otherRule = (RuleResult) other;
 572                         if (itemset != null && otherRule.itemset != null) {
 573                                 // both rules have items, compare these
 574                                 return itemset.compareAndLog(otherRule.itemset);
 575                         } else if (itemset == null && otherRule.itemset == null) {
 576                                 // no items in any of the rules (both null)
 577                                 return true;
 578                         } else {
 579                                 if (itemset == null) {
 580                                         DetailsTracker.log("\n    This item set was null while the other wasn't. The other: "
 581                                                         + otherRule.itemset + ". ");
 582                                 } else {
 583                                         DetailsTracker.log("\n    The other item set was null while this wasn't. This: " + itemset + ". ");
 584                                 }
 585                                 return false;
 586                         }
 587                 }
 588 
 589                 public void toXml(Element parent) {
 590                         Element ruleNode = parent.getOwnerDocument().createElement("rule");
 591                         parent.appendChild(ruleNode);
 592                         ruleNode.appendChild(createValueNode(parent.getOwnerDocument(), "id", id));
 593                         ruleNode.appendChild(createValueNode(parent.getOwnerDocument(), "severity", severity));
 594                         ruleNode.appendChild(createValueNode(parent.getOwnerDocument(), "score", score));
 595                         ruleNode.appendChild(createValueNode(parent.getOwnerDocument(), "shortDescription", shortDescription));
 596                         if (longDescription != null) {
 597                                 ruleNode.appendChild(createValueNode(parent.getOwnerDocument(), "longDescription", longDescription));
 598                         }
 599                         if (itemset != null) {
 600                                 itemset.toXml(ruleNode);
 601                         }
 602                 }
 603 
 604                 public static RuleResult fromXml(Node node) {
 605                         RuleResult rule = null;
 606                         List<String> longDescriptions = getNodeValues("./longDescription", node);
 607                         String longDescription = null;
 608                         if (longDescriptions != null && longDescriptions.size() == 1) {
 609                                 longDescription = longDescriptions.get(0);
 610                         }
 611                         NodeList items = getNodeSet("./itemset", node);
 612                         ItemSet itemset = null;
 613                         if (items != null && items.getLength() == 1) {
 614                                 itemset = ItemSet.fromXml(items.item(0));
 615                         }
 616                         rule = new RuleResult(getNodeValues("./id", node).get(0), getNodeValues("./severity", node).get(0),
 617                                         getNodeValues("./score", node).get(0), getNodeValues("./shortDescription", node).get(0),
 618                                         longDescription, itemset);
 619                         return rule;
 620                 }
 621         }
 622 
 623         private static class ItemSet {
 624                 private List<String> fields;
 625                 private List<ItemList> items;
 626 
 627                 public ItemSet() {
 628                         fields = new ArrayList<>();
 629                         items = new ArrayList<>();
 630                 }
 631 
 632                 private ItemSet(List<String> fields, List<ItemList> items) {
 633                         this.fields = fields;
 634                         this.items = items;
 635                 }
 636 
 637                 public void addField(String field) {
 638                         fields.add(field);
 639                 }
 640 
 641                 public void addItem(ItemList itemList) {
 642                         items.add(itemList);
 643                 }
 644 
 645                 @Override
 646                 public String toString() {
 647                         return "Fields: " + fields + "\n      Items: " + items;
 648                 }
 649 
 650                 public boolean compareAndLog(Object other) {
 651                         ItemSet otherItemSet = (ItemSet) other;
 652                         boolean fieldEquality = fields.equals(otherItemSet.fields);
 653                         if (!fieldEquality) {
 654                                 DetailsTracker.log("Item fields differ: " + fields + " was not equal to " + otherItemSet.fields + ". ");
 655                         }
 656                         boolean itemEquality = items.equals(otherItemSet.items);
 657                         return itemEquality && fieldEquality;
 658                 }
 659 
 660                 public void toXml(Element parent) {
 661                         Element itemSetNode = parent.getOwnerDocument().createElement("itemset");
 662                         parent.appendChild(itemSetNode);
 663                         Element fieldsNode = parent.getOwnerDocument().createElement("fields");
 664                         itemSetNode.appendChild(fieldsNode);
 665                         for (String field : fields) {
 666                                 Element fieldNode = parent.getOwnerDocument().createElement("field");
 667                                 fieldsNode.appendChild(fieldNode);
 668                                 fieldNode.appendChild(createValueNode(parent.getOwnerDocument(), "name", field));
 669                         }
 670                         Element itemsNode = parent.getOwnerDocument().createElement("items");
 671                         itemSetNode.appendChild(itemsNode);
 672                         for (ItemList list : items) {
 673                                 list.toXml(itemsNode);
 674                         }
 675                 }
 676 
 677                 public static ItemSet fromXml(Node node) {
 678                         ItemSet set = null;
 679                         List<ItemList> itemList = new ArrayList<>();
 680                         NodeList items = getNodeSet("./items/item", node);
 681                         for (int i = 0; i < items.getLength(); i++) {
 682                                 Node thisItemOnly = items.item(i);
 683                                 thisItemOnly.getParentNode().removeChild(thisItemOnly);
 684                                 itemList.add(ItemList.fromXml(thisItemOnly));
 685                         }
 686                         List<String> fields = getNodeValues("./fields/field/name", node);
 687                         set = new ItemSet(fields, itemList);
 688                         return set;
 689                 }
 690 
 691         }
 692 
 693         private static class ItemList {
 694                 private List<String> items;
 695 
 696                 public ItemList() {
 697                         items = new ArrayList<>();
 698                 }
 699 
 700                 private ItemList(List<String> list) {
 701                         items = list;
 702                 }
 703 
 704                 public void add(String item) {
 705                         items.add(item);
 706                 }
 707 
 708                 @Override
 709                 public String toString() {
 710                         return items.toString();
 711                 }
 712 
 713                 @Override
 714                 public boolean equals(Object other) {
 715                         ItemList otherItemList = (ItemList) other;
 716                         boolean equals = items.equals(otherItemList.items);
 717                         if (!equals) {
 718                                 DetailsTracker.log("Item lists differ: " + items + " was not equal to " + otherItemList.items + ". ");
 719                         }
 720                         return equals;
 721                 }
 722 
 723                 public void toXml(Element parent) {
 724                         Element itemNode = parent.getOwnerDocument().createElement("item");
 725                         parent.appendChild(itemNode);
 726                         for (String item : items) {
 727                                 itemNode.appendChild(createValueNode(parent.getOwnerDocument(), "value", item));
 728                         }
 729                 }
 730 
 731                 public static ItemList fromXml(Node node) {
 732                         return new ItemList(getNodeValues("./value", node));
 733                 }
 734         }
 735 
 736         // FIXME: This class is not thread safe. Make non-static!
 737         private static class DetailsTracker {
 738                 private static Deque<String> entries = new ArrayDeque<>();
 739 
 740                 private DetailsTracker() {
 741                 }
 742 
 743                 public static void log(String entry) {
 744                         entries.addFirst(entry);
 745                 }
 746 
 747                 public static String getEntries() {
 748                         StringBuilder sb = new StringBuilder();
 749                         for (String entry : entries) {
 750                                 sb.append(entry);
 751                         }
 752                         return sb.toString();
 753                 }
 754 
 755                 public static void clear() {
 756                         entries.clear();
 757                 }
 758         }
 759 
 760 }