001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GraphicsEnvironment; 007import java.io.File; 008import java.io.FileNotFoundException; 009import java.io.IOException; 010import java.nio.charset.StandardCharsets; 011import java.nio.file.Files; 012import java.nio.file.Path; 013import java.nio.file.Paths; 014import java.util.ArrayList; 015import java.util.Arrays; 016import java.util.Collection; 017import java.util.Collections; 018import java.util.EnumMap; 019import java.util.Enumeration; 020import java.util.HashMap; 021import java.util.Iterator; 022import java.util.List; 023import java.util.Map; 024import java.util.Map.Entry; 025import java.util.SortedMap; 026import java.util.TreeMap; 027import java.util.TreeSet; 028import java.util.function.Predicate; 029import java.util.regex.Pattern; 030import java.util.stream.Collectors; 031 032import javax.swing.JOptionPane; 033import javax.swing.JTree; 034import javax.swing.tree.DefaultMutableTreeNode; 035import javax.swing.tree.TreeModel; 036import javax.swing.tree.TreeNode; 037 038import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper; 039import org.openstreetmap.josm.data.projection.ProjectionRegistry; 040import org.openstreetmap.josm.data.validation.tests.Addresses; 041import org.openstreetmap.josm.data.validation.tests.ApiCapabilitiesTest; 042import org.openstreetmap.josm.data.validation.tests.BarriersEntrances; 043import org.openstreetmap.josm.data.validation.tests.Coastlines; 044import org.openstreetmap.josm.data.validation.tests.ConditionalKeys; 045import org.openstreetmap.josm.data.validation.tests.CrossingWays; 046import org.openstreetmap.josm.data.validation.tests.DuplicateNode; 047import org.openstreetmap.josm.data.validation.tests.DuplicateRelation; 048import org.openstreetmap.josm.data.validation.tests.DuplicateWay; 049import org.openstreetmap.josm.data.validation.tests.DuplicatedWayNodes; 050import org.openstreetmap.josm.data.validation.tests.Highways; 051import org.openstreetmap.josm.data.validation.tests.InternetTags; 052import org.openstreetmap.josm.data.validation.tests.Lanes; 053import org.openstreetmap.josm.data.validation.tests.LongSegment; 054import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker; 055import org.openstreetmap.josm.data.validation.tests.MultipolygonTest; 056import org.openstreetmap.josm.data.validation.tests.NameMismatch; 057import org.openstreetmap.josm.data.validation.tests.OpeningHourTest; 058import org.openstreetmap.josm.data.validation.tests.OverlappingWays; 059import org.openstreetmap.josm.data.validation.tests.PowerLines; 060import org.openstreetmap.josm.data.validation.tests.PublicTransportRouteTest; 061import org.openstreetmap.josm.data.validation.tests.RelationChecker; 062import org.openstreetmap.josm.data.validation.tests.RightAngleBuildingTest; 063import org.openstreetmap.josm.data.validation.tests.SelfIntersectingWay; 064import org.openstreetmap.josm.data.validation.tests.SharpAngles; 065import org.openstreetmap.josm.data.validation.tests.SimilarNamedWays; 066import org.openstreetmap.josm.data.validation.tests.TagChecker; 067import org.openstreetmap.josm.data.validation.tests.TurnrestrictionTest; 068import org.openstreetmap.josm.data.validation.tests.UnclosedWays; 069import org.openstreetmap.josm.data.validation.tests.UnconnectedWays; 070import org.openstreetmap.josm.data.validation.tests.UntaggedNode; 071import org.openstreetmap.josm.data.validation.tests.UntaggedWay; 072import org.openstreetmap.josm.data.validation.tests.WayConnectedToArea; 073import org.openstreetmap.josm.data.validation.tests.WronglyOrderedWays; 074import org.openstreetmap.josm.gui.MainApplication; 075import org.openstreetmap.josm.gui.layer.ValidatorLayer; 076import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; 077import org.openstreetmap.josm.gui.util.GuiHelper; 078import org.openstreetmap.josm.spi.preferences.Config; 079import org.openstreetmap.josm.tools.AlphanumComparator; 080import org.openstreetmap.josm.tools.Logging; 081import org.openstreetmap.josm.tools.Utils; 082 083/** 084 * A OSM data validator. 085 * 086 * @author Francisco R. Santos <frsantos@gmail.com> 087 */ 088public final class OsmValidator { 089 090 private OsmValidator() { 091 // Hide default constructor for utilities classes 092 } 093 094 private static volatile ValidatorLayer errorLayer; 095 096 /** Grid detail, multiplier of east,north values for valuable cell sizing */ 097 private static double griddetail; 098 099 private static final SortedMap<String, String> ignoredErrors = new TreeMap<>(); 100 /** 101 * All registered tests 102 */ 103 private static final Collection<Class<? extends Test>> allTests = new ArrayList<>(); 104 private static final Map<String, Test> allTestsMap = new HashMap<>(); 105 106 /** 107 * All available tests in core 108 */ 109 @SuppressWarnings("unchecked") 110 private static final Class<Test>[] CORE_TEST_CLASSES = new Class[] {// NOPMD 111 /* FIXME - unique error numbers for tests aren't properly unique - ignoring will not work as expected */ 112 DuplicateNode.class, // ID 1 .. 99 113 OverlappingWays.class, // ID 101 .. 199 114 UntaggedNode.class, // ID 201 .. 299 115 UntaggedWay.class, // ID 301 .. 399 116 SelfIntersectingWay.class, // ID 401 .. 499 117 DuplicatedWayNodes.class, // ID 501 .. 599 118 CrossingWays.Ways.class, // ID 601 .. 699 119 CrossingWays.Boundaries.class, // ID 601 .. 699 120 CrossingWays.Barrier.class, // ID 601 .. 699 121 CrossingWays.SelfCrossing.class, // ID 601 .. 699 122 SimilarNamedWays.class, // ID 701 .. 799 123 Coastlines.class, // ID 901 .. 999 124 WronglyOrderedWays.class, // ID 1001 .. 1099 125 UnclosedWays.class, // ID 1101 .. 1199 126 TagChecker.class, // ID 1201 .. 1299 127 UnconnectedWays.UnconnectedHighways.class, // ID 1301 .. 1399 128 UnconnectedWays.UnconnectedRailways.class, // ID 1301 .. 1399 129 UnconnectedWays.UnconnectedWaterways.class, // ID 1301 .. 1399 130 UnconnectedWays.UnconnectedNaturalOrLanduse.class, // ID 1301 .. 1399 131 UnconnectedWays.UnconnectedPower.class, // ID 1301 .. 1399 132 DuplicateWay.class, // ID 1401 .. 1499 133 NameMismatch.class, // ID 1501 .. 1599 134 MultipolygonTest.class, // ID 1601 .. 1699 135 RelationChecker.class, // ID 1701 .. 1799 136 TurnrestrictionTest.class, // ID 1801 .. 1899 137 DuplicateRelation.class, // ID 1901 .. 1999 138 WayConnectedToArea.class, // ID 2301 .. 2399 139 PowerLines.class, // ID 2501 .. 2599 140 Addresses.class, // ID 2601 .. 2699 141 Highways.class, // ID 2701 .. 2799 142 BarriersEntrances.class, // ID 2801 .. 2899 143 OpeningHourTest.class, // 2901 .. 2999 144 MapCSSTagChecker.class, // 3000 .. 3099 145 Lanes.class, // 3100 .. 3199 146 ConditionalKeys.class, // 3200 .. 3299 147 InternetTags.class, // 3300 .. 3399 148 ApiCapabilitiesTest.class, // 3400 .. 3499 149 LongSegment.class, // 3500 .. 3599 150 PublicTransportRouteTest.class, // 3600 .. 3699 151 RightAngleBuildingTest.class, // 3700 .. 3799 152 SharpAngles.class, // 3800 .. 3899 153 }; 154 155 /** 156 * Adds a test to the list of available tests 157 * @param testClass The test class 158 */ 159 public static void addTest(Class<? extends Test> testClass) { 160 allTests.add(testClass); 161 try { 162 allTestsMap.put(testClass.getName(), testClass.getConstructor().newInstance()); 163 } catch (ReflectiveOperationException e) { 164 Logging.error(e); 165 } 166 } 167 168 /** 169 * Removes a test from the list of available tests. This will not remove 170 * core tests. 171 * 172 * @param testClass The test class 173 * @return {@code true} if the test was removed (see {@link Collection#remove}) 174 * @since 15603 175 */ 176 public static boolean removeTest(Class<? extends Test> testClass) { 177 boolean removed = false; 178 if (!Arrays.asList(CORE_TEST_CLASSES).contains(testClass)) { 179 removed = allTests.remove(testClass); 180 allTestsMap.remove(testClass.getName()); 181 } 182 return removed; 183 } 184 185 static { 186 for (Class<? extends Test> testClass : CORE_TEST_CLASSES) { 187 addTest(testClass); 188 } 189 } 190 191 /** 192 * Initializes {@code OsmValidator}. 193 */ 194 public static void initialize() { 195 initializeGridDetail(); 196 loadIgnoredErrors(); 197 } 198 199 /** 200 * Returns the validator directory. 201 * 202 * @return The validator directory 203 */ 204 public static String getValidatorDir() { 205 File dir = new File(Config.getDirs().getUserDataDirectory(true), "validator"); 206 try { 207 return dir.getAbsolutePath(); 208 } catch (SecurityException e) { 209 Logging.log(Logging.LEVEL_ERROR, null, e); 210 return dir.getPath(); 211 } 212 } 213 214 private static void loadIgnoredErrors() { 215 ignoredErrors.clear(); 216 if (ValidatorPrefHelper.PREF_USE_IGNORE.get()) { 217 Config.getPref().getListOfMaps(ValidatorPrefHelper.PREF_IGNORELIST).forEach(ignoredErrors::putAll); 218 Path path = Paths.get(getValidatorDir()).resolve("ignorederrors"); 219 try { 220 if (path.toFile().exists()) { 221 try { 222 TreeSet<String> treeSet = new TreeSet<>(); 223 treeSet.addAll(Files.readAllLines(path, StandardCharsets.UTF_8)); 224 treeSet.forEach(ignore -> ignoredErrors.putIfAbsent(ignore, "")); 225 226 saveIgnoredErrors(); 227 Files.deleteIfExists(path); 228 229 } catch (FileNotFoundException e) { 230 Logging.debug(Logging.getErrorMessage(e)); 231 } catch (IOException e) { 232 Logging.error(e); 233 } 234 } 235 } catch (SecurityException e) { 236 Logging.log(Logging.LEVEL_ERROR, "Unable to load ignored errors", e); 237 } 238 } 239 } 240 241 /** 242 * Adds an ignored error 243 * @param s The ignore group / sub group name 244 * @see TestError#getIgnoreGroup() 245 * @see TestError#getIgnoreSubGroup() 246 */ 247 public static void addIgnoredError(String s) { 248 addIgnoredError(s, ""); 249 } 250 251 /** 252 * Adds an ignored error 253 * @param s The ignore group / sub group name 254 * @param description What the error actually is 255 * @see TestError#getIgnoreGroup() 256 * @see TestError#getIgnoreSubGroup() 257 */ 258 public static void addIgnoredError(String s, String description) { 259 if (description == null) description = ""; 260 ignoredErrors.put(s, description); 261 } 262 263 /** 264 * Make sure that we don't keep single entries for a "group ignore". 265 */ 266 static void cleanupIgnoredErrors() { 267 if (ignoredErrors.size() > 1) { 268 List<String> toRemove = new ArrayList<>(); 269 270 Iterator<Entry<String, String>> iter = ignoredErrors.entrySet().iterator(); 271 String lastKey = iter.next().getKey(); 272 while (iter.hasNext()) { 273 String currKey = iter.next().getKey(); 274 if (currKey.startsWith(lastKey) && sameCode(currKey, lastKey)) { 275 toRemove.add(currKey); 276 } else { 277 lastKey = currKey; 278 } 279 } 280 toRemove.forEach(ignoredErrors::remove); 281 } 282 283 Map<String, String> tmap = buildIgnore(buildJTreeList()); 284 if (!tmap.isEmpty()) { 285 ignoredErrors.clear(); 286 ignoredErrors.putAll(tmap); 287 } 288 } 289 290 private static boolean sameCode(String key1, String key2) { 291 return extractCodeFromIgnoreKey(key1).equals(extractCodeFromIgnoreKey(key2)); 292 } 293 294 /** 295 * Extract the leading digits building the code for the error key. 296 * @param key the error key 297 * @return the leading digits 298 */ 299 private static String extractCodeFromIgnoreKey(String key) { 300 int lenCode = 0; 301 302 for (int i = 0; i < key.length(); i++) { 303 if (key.charAt(i) >= '0' && key.charAt(i) <= '9') { 304 lenCode++; 305 } else { 306 break; 307 } 308 } 309 return key.substring(0, lenCode); 310 } 311 312 /** 313 * Check if a error should be ignored 314 * @param s The ignore group / sub group name 315 * @return <code>true</code> to ignore that error 316 */ 317 public static boolean hasIgnoredError(String s) { 318 return ignoredErrors.containsKey(s); 319 } 320 321 /** 322 * Get the list of all ignored errors 323 * @return The <code>Collection<String></code> of errors that are ignored 324 */ 325 public static SortedMap<String, String> getIgnoredErrors() { 326 return ignoredErrors; 327 } 328 329 /** 330 * Build a JTree with a list 331 * @return <type>list as a {@code JTree} 332 */ 333 public static JTree buildJTreeList() { 334 DefaultMutableTreeNode root = new DefaultMutableTreeNode(tr("Ignore list")); 335 final Pattern elemId1Pattern = Pattern.compile(":(r|w|n)_"); 336 final Pattern elemId2Pattern = Pattern.compile("^[0-9]+$"); 337 for (Entry<String, String> e: ignoredErrors.entrySet()) { 338 String key = e.getKey(); 339 // key starts with a code, it maybe followed by a string (eg. a MapCSS rule) and 340 // optionally with a list of one or more OSM element IDs 341 String description = e.getValue(); 342 343 ArrayList<String> ignoredElementList = new ArrayList<>(); 344 String[] osmobjects = elemId1Pattern.split(key); 345 for (int i = 1; i < osmobjects.length; i++) { 346 String osmid = osmobjects[i]; 347 if (elemId2Pattern.matcher(osmid).matches()) { 348 osmid = '_' + osmid; 349 int index = key.indexOf(osmid); 350 if (index < key.lastIndexOf(']')) continue; 351 char type = key.charAt(index - 1); 352 ignoredElementList.add(type + osmid); 353 } 354 } 355 for (String osmignore : ignoredElementList) { 356 key = key.replace(':' + osmignore, ""); 357 } 358 359 DefaultMutableTreeNode trunk; 360 DefaultMutableTreeNode branch; 361 362 if (description != null && !description.isEmpty()) { 363 trunk = inTree(root, description); 364 branch = inTree(trunk, key); 365 trunk.add(branch); 366 } else { 367 trunk = inTree(root, key); 368 branch = trunk; 369 } 370 if (!ignoredElementList.isEmpty()) { 371 String item; 372 if (ignoredElementList.size() == 1) { 373 item = ignoredElementList.iterator().next(); 374 } else { 375 // combination of two or more objects, keep them together 376 item = ignoredElementList.toString(); // [ID1, ID2, ..., IDn] 377 } 378 branch.add(new DefaultMutableTreeNode(item)); 379 } 380 root.add(trunk); 381 } 382 return new JTree(root); 383 } 384 385 private static DefaultMutableTreeNode inTree(DefaultMutableTreeNode root, String name) { 386 @SuppressWarnings("unchecked") 387 Enumeration<TreeNode> trunks = root.children(); 388 while (trunks.hasMoreElements()) { 389 TreeNode ttrunk = trunks.nextElement(); 390 if (ttrunk instanceof DefaultMutableTreeNode) { 391 DefaultMutableTreeNode trunk = (DefaultMutableTreeNode) ttrunk; 392 if (name.equals(trunk.getUserObject())) { 393 return trunk; 394 } 395 } 396 } 397 return new DefaultMutableTreeNode(name); 398 } 399 400 /** 401 * Build a {@code HashMap} from a tree of ignored errors 402 * @param tree The JTree of ignored errors 403 * @return A {@code HashMap} of the ignored errors for comparison 404 */ 405 public static Map<String, String> buildIgnore(JTree tree) { 406 TreeModel model = tree.getModel(); 407 DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot(); 408 return buildIgnore(model, root); 409 } 410 411 private static Map<String, String> buildIgnore(TreeModel model, DefaultMutableTreeNode node) { 412 HashMap<String, String> rHashMap = new HashMap<>(); 413 414 for (int i = 0; i < model.getChildCount(node); i++) { 415 DefaultMutableTreeNode child = (DefaultMutableTreeNode) model.getChild(node, i); 416 if (model.getChildCount(child) == 0) { 417 // create an entry for the error list 418 String key = node.getUserObject().toString(); 419 String description; 420 421 if (!model.getRoot().equals(node)) { 422 description = ((DefaultMutableTreeNode) node.getParent()).getUserObject().toString(); 423 } else { 424 description = key; // we get here when reading old file ignorederrors 425 } 426 if (tr("Ignore list").equals(description)) 427 description = ""; 428 if (!key.matches("^[0-9]+(_.*|$)")) { 429 description = key; 430 key = ""; 431 } 432 433 String item = child.getUserObject().toString(); 434 String entry = null; 435 if (item.matches("^\\[(r|w|n)_.*")) { 436 // list of elements (produced with list.toString() method) 437 entry = key + ":" + item.substring(1, item.lastIndexOf(']')).replace(", ", ":"); 438 } else if (item.matches("^(r|w|n)_.*")) { 439 // single element 440 entry = key + ":" + item; 441 } else if (item.matches("^[0-9]+(_.*|)$")) { 442 // no element ids 443 entry = item; 444 } 445 if (entry != null) { 446 rHashMap.put(entry, description); 447 } else { 448 Logging.warn("ignored unexpected item in validator ignore list management dialog:'" + item + "'"); 449 } 450 } else { 451 rHashMap.putAll(buildIgnore(model, child)); 452 } 453 } 454 return rHashMap; 455 } 456 457 /** 458 * Reset the error list by deleting {@code validator.ignorelist} 459 */ 460 public static void resetErrorList() { 461 saveIgnoredErrors(); 462 Config.getPref().putListOfMaps(ValidatorPrefHelper.PREF_IGNORELIST, null); 463 OsmValidator.initialize(); 464 } 465 466 /** 467 * Saves the names of the ignored errors to a preference 468 */ 469 public static void saveIgnoredErrors() { 470 List<Map<String, String>> list = new ArrayList<>(); 471 cleanupIgnoredErrors(); 472 list.add(ignoredErrors); 473 int i = 0; 474 while (i < list.size()) { 475 if (list.get(i) == null || list.get(i).isEmpty()) { 476 list.remove(i); 477 continue; 478 } 479 i++; 480 } 481 if (list.isEmpty()) list = null; 482 Config.getPref().putListOfMaps(ValidatorPrefHelper.PREF_IGNORELIST, list); 483 } 484 485 /** 486 * Initializes error layer. 487 */ 488 public static synchronized void initializeErrorLayer() { 489 if (!ValidatorPrefHelper.PREF_LAYER.get()) 490 return; 491 if (errorLayer == null) { 492 errorLayer = new ValidatorLayer(); 493 MainApplication.getLayerManager().addLayer(errorLayer); 494 } 495 } 496 497 /** 498 * Resets error layer. 499 * @since 11852 500 */ 501 public static synchronized void resetErrorLayer() { 502 errorLayer = null; 503 } 504 505 /** 506 * Gets a map from simple names to all tests. 507 * @return A map of all tests, indexed and sorted by the name of their Java class 508 */ 509 public static SortedMap<String, Test> getAllTestsMap() { 510 applyPrefs(allTestsMap, false); 511 applyPrefs(allTestsMap, true); 512 return new TreeMap<>(allTestsMap); 513 } 514 515 /** 516 * Returns the instance of the given test class. 517 * @param <T> testClass type 518 * @param testClass The class of test to retrieve 519 * @return the instance of the given test class, if any, or {@code null} 520 * @since 6670 521 */ 522 @SuppressWarnings("unchecked") 523 public static <T extends Test> T getTest(Class<T> testClass) { 524 if (testClass == null) { 525 return null; 526 } 527 return (T) allTestsMap.get(testClass.getName()); 528 } 529 530 private static void applyPrefs(Map<String, Test> tests, boolean beforeUpload) { 531 for (String testName : Config.getPref().getList(beforeUpload 532 ? ValidatorPrefHelper.PREF_SKIP_TESTS_BEFORE_UPLOAD : ValidatorPrefHelper.PREF_SKIP_TESTS)) { 533 Test test = tests.get(testName); 534 if (test != null) { 535 if (beforeUpload) { 536 test.testBeforeUpload = false; 537 } else { 538 test.enabled = false; 539 } 540 } 541 } 542 } 543 544 /** 545 * Gets all tests that are possible 546 * @return The tests 547 */ 548 public static Collection<Test> getTests() { 549 return getAllTestsMap().values(); 550 } 551 552 /** 553 * Gets all tests that are run 554 * @param beforeUpload To get the ones that are run before upload 555 * @return The tests 556 */ 557 public static Collection<Test> getEnabledTests(boolean beforeUpload) { 558 Collection<Test> enabledTests = getTests(); 559 for (Test t : new ArrayList<>(enabledTests)) { 560 if (beforeUpload ? t.testBeforeUpload : t.enabled) { 561 continue; 562 } 563 enabledTests.remove(t); 564 } 565 return enabledTests; 566 } 567 568 /** 569 * Gets the list of all available test classes 570 * 571 * @return A collection of the test classes 572 */ 573 public static Collection<Class<? extends Test>> getAllAvailableTestClasses() { 574 return Collections.unmodifiableCollection(allTests); 575 } 576 577 /** 578 * Initialize grid details based on current projection system. Values based on 579 * the original value fixed for EPSG:4326 (10000) using heuristics (that is, test&error 580 * until most bugs were discovered while keeping the processing time reasonable) 581 */ 582 public static void initializeGridDetail() { 583 String code = ProjectionRegistry.getProjection().toCode(); 584 if (Arrays.asList(ProjectionPreference.wgs84.allCodes()).contains(code)) { 585 OsmValidator.griddetail = 10_000; 586 } else if (Arrays.asList(ProjectionPreference.mercator.allCodes()).contains(code)) { 587 OsmValidator.griddetail = 0.01; 588 } else if (Arrays.asList(ProjectionPreference.lambert.allCodes()).contains(code)) { 589 OsmValidator.griddetail = 0.1; 590 } else { 591 OsmValidator.griddetail = 1.0; 592 } 593 } 594 595 /** 596 * Returns grid detail, multiplier of east,north values for valuable cell sizing 597 * @return grid detail 598 * @since 11852 599 */ 600 public static double getGridDetail() { 601 return griddetail; 602 } 603 604 private static boolean testsInitialized; 605 606 /** 607 * Initializes all tests if this operations hasn't been performed already. 608 */ 609 public static synchronized void initializeTests() { 610 if (!testsInitialized) { 611 Logging.debug("Initializing validator tests"); 612 final long startTime = System.currentTimeMillis(); 613 initializeTests(getTests()); 614 testsInitialized = true; 615 if (Logging.isDebugEnabled()) { 616 final long elapsedTime = System.currentTimeMillis() - startTime; 617 Logging.debug("Initializing validator tests completed in {0}", Utils.getDurationString(elapsedTime)); 618 } 619 } 620 } 621 622 /** 623 * Initializes all tests 624 * @param allTests The tests to initialize 625 */ 626 public static void initializeTests(Collection<? extends Test> allTests) { 627 for (Test test : allTests) { 628 try { 629 if (test.enabled) { 630 test.initialize(); 631 } 632 } catch (Exception e) { // NOPMD 633 String message = tr("Error initializing test {0}:\n {1}", test.getClass().getSimpleName(), e); 634 Logging.error(message); 635 if (!GraphicsEnvironment.isHeadless()) { 636 GuiHelper.runInEDT(() -> 637 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), message, tr("Error"), JOptionPane.ERROR_MESSAGE) 638 ); 639 } 640 } 641 } 642 } 643 644 /** 645 * Groups the given collection of errors by severity, then message, then description. 646 * @param errors list of errors to group 647 * @param filterToUse optional filter 648 * @return collection of errors grouped by severity, then message, then description 649 * @since 12667 650 */ 651 public static Map<Severity, Map<String, Map<String, List<TestError>>>> getErrorsBySeverityMessageDescription( 652 Collection<TestError> errors, Predicate<? super TestError> filterToUse) { 653 return errors.stream().filter(filterToUse).collect( 654 Collectors.groupingBy(TestError::getSeverity, () -> new EnumMap<>(Severity.class), 655 Collectors.groupingBy(TestError::getMessage, () -> new TreeMap<>(AlphanumComparator.getInstance()), 656 Collectors.groupingBy(e -> e.getDescription() == null ? "" : e.getDescription(), 657 () -> new TreeMap<>(AlphanumComparator.getInstance()), 658 Collectors.toList() 659 )))); 660 } 661 662 /** 663 * For unit tests 664 */ 665 static void clearIgnoredErrors() { 666 ignoredErrors.clear(); 667 } 668}