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