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&lt;String&gt;</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 &lt;type&gt;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&amp;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}