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                 // FIXME : JMC-6072
 160                 // Assert.assertTrue(DetailsTracker.getEntries(), resultsEqual);
 161         }
 162 
 163         private static void saveToFile(Document doc, String directory, String fileName, boolean onlyOneRecording) {
 164                 String filePath = getResultDir().getAbsolutePath() + File.separator
 165                                 + ((directory != null) ? (directory + File.separator) : "")
 166                                 + (onlyOneRecording ? "Generated_One_" : "Generated_") + fileName;
 167                 File resultFile = new File(filePath);
 168                 prepareFile(resultFile);
 169                 try {
 170                         writeDomToStream(doc, new FileOutputStream(resultFile));
 171                 } catch (FileNotFoundException e) {
 172                         e.printStackTrace();
 173                 }
 174         }
 175 
 176         private static void prepareFile(File file) {
 177                 if (file.exists()) {
 178                         file.delete();
 179                 }
 180                 File parent = file.getParentFile();
 181                 if (parent != null) {
 182                         parent.mkdirs();
 183                 }
 184                 try {
 185                         file.createNewFile();
 186                 } catch (IOException e) {
 187                         e.printStackTrace();
 188                         Assert.fail("Error creating file \"" + file.getAbsolutePath() + "\". Error:\n" + e.getMessage());
 189                 }
 190         }
 191 
 192         private static void writeDomToStream(Document doc, OutputStream os) {
 193                 try {
 194                         TransformerFactory transformerFactory = TransformerFactory.newInstance();
 195                         Transformer transformer = transformerFactory.newTransformer();
 196                         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
 197                         DOMSource source = new DOMSource(doc);
 198                         StreamResult console = new StreamResult(os);
 199                         transformer.transform(source, console);
 200                 } catch (TransformerException e) {
 201                         e.printStackTrace();
 202                 }
 203         }
 204 
 205         private static ReportCollection parseRulesReportXml(String directory, String fileName, String reportName) {
 206                 ReportCollection collection = new ReportCollection();
 207                 try {
 208                         // FIXME: No need to go via temp file. Just get the input stream directly from the resource.
 209                         File dir = TestToolkit.materialize(TestRulesWithJfr.class, directory, fileName);
 210                         File file = new File(dir, fileName);
 211                         DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
 212                         DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
 213                         Document baselineDoc = docBuilder.parse(file);
 214                         collection = ReportCollection.fromXml(baselineDoc, reportName);
 215                 } catch (ParserConfigurationException | SAXException | IOException e) {
 216                         e.printStackTrace();
 217                 }
 218                 return collection;
 219         }
 220 
 221         private static ReportCollection generateRulesReport(IOResourceSet jfrs) {
 222                 ReportCollection collection = new ReportCollection();
 223                 for (IOResource jfr : jfrs) {
 224                         Report report = generateReport(jfr, false, null);
 225                         collection.put(report.getName(), report);
 226                 }
 227                 return collection;
 228         }
 229 
 230         private static File getResultDir() {
 231                 if (System.getProperty("results.dir") != null) {
 232                         return new File(System.getProperty("results.dir"));
 233                 } else {
 234                         return new File(System.getProperty("user.dir"));
 235                 }
 236         }
 237 
 238         private static Report generateReport(IOResource jfr, boolean verbose, Severity minSeverity) {
 239                 Report report = new Report(jfr.getName());
 240                 try {
 241                         IItemCollection events = JfrLoaderToolkit.loadEvents(jfr.open());
 242 
 243                         for (IRule rule : RuleRegistry.getRules()) {
 244                                 try {
 245                                         RunnableFuture<Result> future = rule.evaluate(events,
 246                                                         IPreferenceValueProvider.DEFAULT_VALUES);
 247                                         future.run();
 248                                         Result result = future.get();
 249 //                                      for (Result result : results) {
 250                                         if (minSeverity == null || Severity.get(result.getScore()).compareTo(minSeverity) >= 0) {
 251                                                 ItemSet itemSet = null;
 252                                                 IItemQuery itemQuery = result.getItemQuery();
 253                                                 if (verbose && itemQuery != null && !itemQuery.getAttributes().isEmpty()) {
 254                                                         itemSet = new ItemSet();
 255                                                         IItemCollection resultEvents = events.apply(itemQuery.getFilter());
 256                                                         Collection<? extends IAttribute<?>> attributes = itemQuery.getAttributes();
 257                                                         for (IAttribute<?> attribute : attributes) {
 258                                                                 itemSet.addField(attribute.getName());
 259                                                         }
 260                                                         Iterator<? extends IItemIterable> iterables = resultEvents.iterator();
 261                                                         while (iterables.hasNext()) {
 262                                                                 IItemIterable ii = iterables.next();
 263                                                                 IType<IItem> type = ii.getType();
 264                                                                 List<IMemberAccessor<?, IItem>> accessors = new ArrayList<>(attributes.size());
 265                                                                 for (IAttribute<?> a : attributes) {
 266                                                                         accessors.add(a.getAccessor(type));
 267                                                                 }
 268                                                                 Iterator<? extends IItem> items = ii.iterator();
 269                                                                 while (items.hasNext()) {
 270                                                                         ItemList itemList = new ItemList();
 271                                                                         IItem item = items.next();
 272                                                                         for (IMemberAccessor<?, IItem> a : accessors) {
 273                                                                                 itemList.add(String.valueOf(a.getMember(item)));
 274                                                                         }
 275                                                                         itemSet.addItem(itemList);
 276                                                                 }
 277                                                         }
 278                                                 }
 279                                                 RuleResult ruleResult = new RuleResult(String.valueOf(result.getRule().getId()),
 280                                                                 Severity.get(result.getScore()).getLocalizedName(), String.valueOf(result.getScore()),
 281                                                                 result.getShortDescription(), result.getLongDescription(), itemSet);
 282                                                 report.put(String.valueOf(result.getRule().getId()), ruleResult);
 283 //                                              }
 284                                         }
 285                                 } catch (RuntimeException | InterruptedException | ExecutionException e) {
 286                                         System.out.println("Problem while evaluating rules for \"" + jfr.getName() + "\". Message: "
 287                                                         + e.getLocalizedMessage());
 288                                 }
 289                         }
 290                 } catch (IOException | CouldNotLoadRecordingException e) {
 291                         e.printStackTrace();
 292                 }
 293                 return report;
 294         }
 295 
 296         private static Element createValueNode(Document doc, String name, String value) {
 297                 Element node = doc.createElement(name);
 298                 node.appendChild(doc.createTextNode(value != null ? value : ""));
 299                 return node;
 300         }
 301 
 302         private static List<String> getNodeValues(String xpathExpr, Node node) {
 303                 List<String> values = new ArrayList<>();
 304                 try {
 305                         XPath xpath = XPathFactory.newInstance().newXPath();
 306                         XPathExpression expression = xpath.compile(xpathExpr);
 307                         NodeList nodes = ((NodeList) expression.evaluate(node, XPathConstants.NODESET));
 308                         for (int i = 0; i < nodes.getLength(); i++) {
 309                                 Node thisNodeOnly = nodes.item(i);
 310                                 thisNodeOnly.getParentNode().removeChild(thisNodeOnly);
 311                                 Node child = thisNodeOnly.getFirstChild();
 312                                 if (child != null) {
 313                                         values.add(child.getNodeValue());
 314                                 } else {
 315                                         values.add("");
 316                                 }
 317                         }
 318                 } catch (XPathExpressionException e) {
 319                         e.printStackTrace();
 320                 }
 321                 return values;
 322         }
 323 
 324         private static NodeList getNodeSet(String expr, Node node) {
 325                 NodeList result = null;
 326                 try {
 327                         XPath xpath = XPathFactory.newInstance().newXPath();
 328                         XPathExpression xPath = xpath.compile(expr);
 329                         result = (NodeList) xPath.evaluate(node, XPathConstants.NODESET);
 330                 } catch (XPathExpressionException e) {
 331                         e.printStackTrace();
 332                 }
 333                 return result;
 334         }
 335 
 336         private static class ReportCollection {
 337                 private SortedMap<String, Report> reports;
 338 
 339                 public ReportCollection() {
 340                         reports = new TreeMap<>();
 341                 }
 342 
 343                 public void put(String filename, Report report) {
 344                         reports.put(filename, report);
 345                 }
 346 
 347                 public Report get(String filename) {
 348                         return reports.get(filename);
 349                 }
 350 
 351                 public boolean compareAndLog(Object other) {
 352                         ReportCollection otherReportCollection = (ReportCollection) other;
 353                         boolean equals = reports.size() == otherReportCollection.reports.size();
 354                         if (!equals) {
 355                                 if (reports.size() > otherReportCollection.reports.size()) {
 356                                         for (String reportname : reports.keySet()) {
 357                                                 if (otherReportCollection.get(reportname) == null) {
 358                                                         DetailsTracker.log("Report for " + reportname
 359                                                                         + " could not be found in the other report collection. ");
 360                                                 }
 361                                         }
 362                                 } else {
 363                                         for (String reportname : otherReportCollection.reports.keySet()) {
 364                                                 if (reports.get(reportname) == null) {
 365                                                         DetailsTracker.log(
 366                                                                         "Report for " + reportname + " could not be found in this report collection. ");
 367                                                 }
 368                                         }
 369                                 }
 370                                 DetailsTracker.log("\n");
 371                         }
 372                         for (String reportname : reports.keySet()) {
 373                                 Report otherReport = otherReportCollection.get(reportname);
 374                                 if (otherReport != null) {
 375                                         equals = reports.get(reportname).compareAndLog(otherReport) && equals;
 376                                 } else {
 377                                         DetailsTracker
 378                                                         .log("\nReport for " + reportname + " could not be found in the other report collection. ");
 379                                         equals = false;
 380                                 }
 381                         }
 382                         return equals;
 383                 }
 384 
 385                 public Document toXml() {
 386                         DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
 387                         DocumentBuilder docBuilder;
 388                         Document doc = null;
 389                         try {
 390                                 docBuilder = docFactory.newDocumentBuilder();
 391                                 doc = docBuilder.newDocument();
 392                                 Element rootElement = doc.createElement("reportcollection");
 393                                 doc.appendChild(rootElement);
 394                                 for (Report report : reports.values()) {
 395                                         report.toXml(rootElement);
 396                                 }
 397                         } catch (ParserConfigurationException e) {
 398                                 e.printStackTrace();
 399                         }
 400                         return doc;
 401                 }
 402 
 403                 public static ReportCollection fromXml(Document doc, String reportName) {
 404                         ReportCollection collection = new ReportCollection();
 405                         NodeList reports = getNodeSet("//report", doc);
 406                         for (int i = 0; i < reports.getLength(); i++) {
 407                                 Node thisReportOnly = reports.item(i);
 408                                 thisReportOnly.getParentNode().removeChild(thisReportOnly);
 409                                 Report report = Report.fromXml(thisReportOnly);
 410                                 if (reportName == null || report.getName().equals(reportName)) {
 411                                         collection.put(report.getName(), report);
 412                                 }
 413                         }
 414                         return collection;
 415                 }
 416         }
 417 
 418         private static class Report {
 419                 private String filename;
 420                 private SortedMap<String, RuleResult> rules;
 421 
 422                 public Report(String filename) {
 423                         this.filename = filename;
 424                         rules = new TreeMap<>();
 425                 }
 426 
 427                 public void put(String id, RuleResult rule) {
 428                         rules.put(id, rule);
 429                 }
 430 
 431                 public RuleResult get(String id) {
 432                         return rules.get(id);
 433                 }
 434 
 435                 public String getName() {
 436                         return filename;
 437                 }
 438 
 439                 public boolean compareAndLog(Object other) {
 440                         Report otherReport = (Report) other;
 441                         boolean equals = rules.size() == otherReport.rules.size();
 442                         boolean fileNamePrinted = false;
 443                         if (equals) {
 444                                 for (String rulename : rules.keySet()) {
 445                                         RuleResult otherRule = otherReport.get(rulename);
 446                                         if (otherRule != null) {
 447                                                 equals = rules.get(rulename).compareAndLog(otherRule) && equals;
 448                                                 if (!equals && !fileNamePrinted) {
 449                                                         DetailsTracker.log("\n\nReport: \"" + filename + "\", ");
 450                                                         fileNamePrinted = true;
 451                                                 }
 452                                         } else {
 453                                                 DetailsTracker.log("\n\nReport: \"" + filename + "\". Rule result for " + rulename
 454                                                                 + " could not be found in the other report. ");
 455                                                 equals = false;
 456                                         }
 457                                 }
 458                         } else {
 459                                 if (rules.size() > otherReport.rules.size()) {
 460                                         for (String ruleId : rules.keySet()) {
 461                                                 RuleResult otherRule = otherReport.get(ruleId);
 462                                                 if (otherRule != null) {
 463                                                         equals = rules.get(ruleId).compareAndLog(otherRule) && equals;
 464                                                 } else {
 465                                                         DetailsTracker.log("\nReport for file \"" + filename + "\", rule result for \"" + ruleId
 466                                                                         + "\" could not be found in the other report. ");
 467                                                 }
 468                                         }
 469                                 } else {
 470                                         for (String ruleId : otherReport.rules.keySet()) {
 471                                                 RuleResult rule = rules.get(ruleId);
 472                                                 if (rule != null) {
 473                                                         equals = rule.compareAndLog(otherReport.rules.get(ruleId)) && equals;
 474                                                 } else {
 475                                                         DetailsTracker.log("\nReport for file \"" + filename + "\", rule result for \"" + ruleId
 476                                                                         + "\" could not be found in this report. ");
 477                                                 }
 478                                         }
 479                                 }
 480                                 DetailsTracker.log("\n");
 481                         }
 482                         return equals;
 483                 }
 484 
 485                 public void toXml(Element parent) {
 486                         Element reportNode = parent.getOwnerDocument().createElement("report");
 487                         parent.appendChild(reportNode);
 488                         reportNode.appendChild(createValueNode(parent.getOwnerDocument(), "file", filename));
 489                         for (RuleResult rule : rules.values()) {
 490                                 rule.toXml(reportNode);
 491                         }
 492                 }
 493 
 494                 public static Report fromXml(Node node) {
 495                         Report report = new Report(getNodeValues("./file", node).get(0));
 496                         NodeList rules = getNodeSet("./rule", node);
 497                         for (int i = 0; i < rules.getLength(); i++) {
 498                                 Node thisRuleOnly = rules.item(i);
 499                                 thisRuleOnly.getParentNode().removeChild(thisRuleOnly);
 500                                 RuleResult rule = RuleResult.fromXml(thisRuleOnly);
 501                                 report.put(rule.getId(), rule);
 502                         }
 503                         return report;
 504                 }
 505         }
 506 
 507         private static class RuleResult {
 508                 private String id;
 509                 private String severity;
 510                 private String score;
 511                 private String shortDescription;
 512                 private String longDescription;
 513                 private ItemSet itemset;
 514 
 515                 public RuleResult(String id, String severity, String score, String shortDescription, String longDescription,
 516                                 ItemSet itemset) {
 517                         this.id = id;
 518                         this.severity = severity;
 519                         this.score = score;
 520                         this.shortDescription = shortDescription;
 521                         this.longDescription = longDescription;
 522                         this.itemset = itemset;
 523                 }
 524 
 525                 public String getId() {
 526                         return id;
 527                 }
 528 
 529                 public boolean compareAndLog(Object other) {
 530                         RuleResult otherRule = (RuleResult) other;
 531                         boolean scoreEquals = Objects.equals(score, otherRule.score);
 532                         if (!scoreEquals) {
 533                                 // determine if this is just a rounding error
 534                                 scoreEquals = (Math.abs(Float.valueOf(score) - Float.valueOf(otherRule.score)) < 0.0000000000001f) ? true
 535                                                 : false;
 536                                 if (scoreEquals) {
 537                                         // apparently a rounding issue. Print it out for informational purposes
 538                                         System.out
 539                                                         .println("Rule \"" + id + "\": Encountered rounding issue for score when comparing values "
 540                                                                         + score + " and " + otherRule.score);
 541                                 }
 542                         }
 543                         boolean itemSetEquality = compareAndLogItemSets(other);
 544                         boolean ruleEquality = Objects.equals(severity, otherRule.severity) && scoreEquals
 545                                         && Objects.equals(shortDescription, otherRule.shortDescription)
 546                                         && Objects.equals(longDescription, otherRule.longDescription);
 547                         if (!ruleEquality) {
 548                                 if (!Objects.equals(severity, otherRule.severity)) {
 549                                         DetailsTracker.log("\n    Severity mismatch: \"" + severity + "\" was not equal to \""
 550                                                         + otherRule.severity + "\". ");
 551                                 }
 552                                 if (!scoreEquals) {
 553                                         DetailsTracker.log(
 554                                                         "\n    Score mismatch: \"" + score + "\" was not equal to \"" + otherRule.score + "\". ");
 555                                 }
 556                                 if (!Objects.equals(shortDescription, otherRule.shortDescription)) {
 557                                         DetailsTracker.log("\n    Message mismatch: \"" + shortDescription + "\" was not equal to \""
 558                                                         + otherRule.shortDescription + "\". ");
 559                                 }
 560                                 if (!Objects.equals(longDescription, otherRule.longDescription)) {
 561                                         DetailsTracker.log("\n    Description mismatch: \"" + longDescription + "\" was not equal to \""
 562                                                         + otherRule.longDescription + "\". ");
 563                                 }
 564                         }
 565                         if (!(itemSetEquality && ruleEquality)) {
 566                                 DetailsTracker.log("\n  Rule: \"" + id + "\". ");
 567                         }
 568                         return itemSetEquality && ruleEquality;
 569                 }
 570 
 571                 private boolean compareAndLogItemSets(Object other) {
 572                         RuleResult otherRule = (RuleResult) other;
 573                         if (itemset != null && otherRule.itemset != null) {
 574                                 // both rules have items, compare these
 575                                 return itemset.compareAndLog(otherRule.itemset);
 576                         } else if (itemset == null && otherRule.itemset == null) {
 577                                 // no items in any of the rules (both null)
 578                                 return true;
 579                         } else {
 580                                 if (itemset == null) {
 581                                         DetailsTracker.log("\n    This item set was null while the other wasn't. The other: "
 582                                                         + otherRule.itemset + ". ");
 583                                 } else {
 584                                         DetailsTracker.log("\n    The other item set was null while this wasn't. This: " + itemset + ". ");
 585                                 }
 586                                 return false;
 587                         }
 588                 }
 589 
 590                 public void toXml(Element parent) {
 591                         Element ruleNode = parent.getOwnerDocument().createElement("rule");
 592                         parent.appendChild(ruleNode);
 593                         ruleNode.appendChild(createValueNode(parent.getOwnerDocument(), "id", id));
 594                         ruleNode.appendChild(createValueNode(parent.getOwnerDocument(), "severity", severity));
 595                         ruleNode.appendChild(createValueNode(parent.getOwnerDocument(), "score", score));
 596                         ruleNode.appendChild(createValueNode(parent.getOwnerDocument(), "shortDescription", shortDescription));
 597                         if (longDescription != null) {
 598                                 ruleNode.appendChild(createValueNode(parent.getOwnerDocument(), "longDescription", longDescription));
 599                         }
 600                         if (itemset != null) {
 601                                 itemset.toXml(ruleNode);
 602                         }
 603                 }
 604 
 605                 public static RuleResult fromXml(Node node) {
 606                         RuleResult rule = null;
 607                         List<String> longDescriptions = getNodeValues("./longDescription", node);
 608                         String longDescription = null;
 609                         if (longDescriptions != null && longDescriptions.size() == 1) {
 610                                 longDescription = longDescriptions.get(0);
 611                         }
 612                         NodeList items = getNodeSet("./itemset", node);
 613                         ItemSet itemset = null;
 614                         if (items != null && items.getLength() == 1) {
 615                                 itemset = ItemSet.fromXml(items.item(0));
 616                         }
 617                         rule = new RuleResult(getNodeValues("./id", node).get(0), getNodeValues("./severity", node).get(0),
 618                                         getNodeValues("./score", node).get(0), getNodeValues("./shortDescription", node).get(0),
 619                                         longDescription, itemset);
 620                         return rule;
 621                 }
 622         }
 623 
 624         private static class ItemSet {
 625                 private List<String> fields;
 626                 private List<ItemList> items;
 627 
 628                 public ItemSet() {
 629                         fields = new ArrayList<>();
 630                         items = new ArrayList<>();
 631                 }
 632 
 633                 private ItemSet(List<String> fields, List<ItemList> items) {
 634                         this.fields = fields;
 635                         this.items = items;
 636                 }
 637 
 638                 public void addField(String field) {
 639                         fields.add(field);
 640                 }
 641 
 642                 public void addItem(ItemList itemList) {
 643                         items.add(itemList);
 644                 }
 645 
 646                 @Override
 647                 public String toString() {
 648                         return "Fields: " + fields + "\n      Items: " + items;
 649                 }
 650 
 651                 public boolean compareAndLog(Object other) {
 652                         ItemSet otherItemSet = (ItemSet) other;
 653                         boolean fieldEquality = fields.equals(otherItemSet.fields);
 654                         if (!fieldEquality) {
 655                                 DetailsTracker.log("Item fields differ: " + fields + " was not equal to " + otherItemSet.fields + ". ");
 656                         }
 657                         boolean itemEquality = items.equals(otherItemSet.items);
 658                         return itemEquality && fieldEquality;
 659                 }
 660 
 661                 public void toXml(Element parent) {
 662                         Element itemSetNode = parent.getOwnerDocument().createElement("itemset");
 663                         parent.appendChild(itemSetNode);
 664                         Element fieldsNode = parent.getOwnerDocument().createElement("fields");
 665                         itemSetNode.appendChild(fieldsNode);
 666                         for (String field : fields) {
 667                                 Element fieldNode = parent.getOwnerDocument().createElement("field");
 668                                 fieldsNode.appendChild(fieldNode);
 669                                 fieldNode.appendChild(createValueNode(parent.getOwnerDocument(), "name", field));
 670                         }
 671                         Element itemsNode = parent.getOwnerDocument().createElement("items");
 672                         itemSetNode.appendChild(itemsNode);
 673                         for (ItemList list : items) {
 674                                 list.toXml(itemsNode);
 675                         }
 676                 }
 677 
 678                 public static ItemSet fromXml(Node node) {
 679                         ItemSet set = null;
 680                         List<ItemList> itemList = new ArrayList<>();
 681                         NodeList items = getNodeSet("./items/item", node);
 682                         for (int i = 0; i < items.getLength(); i++) {
 683                                 Node thisItemOnly = items.item(i);
 684                                 thisItemOnly.getParentNode().removeChild(thisItemOnly);
 685                                 itemList.add(ItemList.fromXml(thisItemOnly));
 686                         }
 687                         List<String> fields = getNodeValues("./fields/field/name", node);
 688                         set = new ItemSet(fields, itemList);
 689                         return set;
 690                 }
 691 
 692         }
 693 
 694         private static class ItemList {
 695                 private List<String> items;
 696 
 697                 public ItemList() {
 698                         items = new ArrayList<>();
 699                 }
 700 
 701                 private ItemList(List<String> list) {
 702                         items = list;
 703                 }
 704 
 705                 public void add(String item) {
 706                         items.add(item);
 707                 }
 708 
 709                 @Override
 710                 public String toString() {
 711                         return items.toString();
 712                 }
 713 
 714                 @Override
 715                 public boolean equals(Object other) {
 716                         ItemList otherItemList = (ItemList) other;
 717                         boolean equals = items.equals(otherItemList.items);
 718                         if (!equals) {
 719                                 DetailsTracker.log("Item lists differ: " + items + " was not equal to " + otherItemList.items + ". ");
 720                         }
 721                         return equals;
 722                 }
 723 
 724                 public void toXml(Element parent) {
 725                         Element itemNode = parent.getOwnerDocument().createElement("item");
 726                         parent.appendChild(itemNode);
 727                         for (String item : items) {
 728                                 itemNode.appendChild(createValueNode(parent.getOwnerDocument(), "value", item));
 729                         }
 730                 }
 731 
 732                 public static ItemList fromXml(Node node) {
 733                         return new ItemList(getNodeValues("./value", node));
 734                 }
 735         }
 736 
 737         // FIXME: This class is not thread safe. Make non-static!
 738         private static class DetailsTracker {
 739                 private static Deque<String> entries = new ArrayDeque<>();
 740 
 741                 private DetailsTracker() {
 742                 }
 743 
 744                 public static void log(String entry) {
 745                         entries.addFirst(entry);
 746                 }
 747 
 748                 public static String getEntries() {
 749                         StringBuilder sb = new StringBuilder();
 750                         for (String entry : entries) {
 751                                 sb.append(entry);
 752                         }
 753                         return sb.toString();
 754                 }
 755 
 756                 public static void clear() {
 757                         entries.clear();
 758                 }
 759         }
 760 
 761 }