source: josm/trunk/src/org/openstreetmap/josm/data/validation/OsmValidator.java@ 15588

Last change on this file since 15588 was 15588, checked in by Don-vip, 4 years ago

tools update: checkstyle 8.27, pmd 6.20, spotbugs 3.1.12

  • Property svn:eol-style set to native
File size: 24.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.GraphicsEnvironment;
7import java.io.File;
8import java.io.FileNotFoundException;
9import java.io.IOException;
10import java.nio.charset.StandardCharsets;
11import java.nio.file.Files;
12import java.nio.file.Path;
13import java.nio.file.Paths;
14import java.util.ArrayList;
15import java.util.Arrays;
16import java.util.Collection;
17import java.util.Collections;
18import java.util.EnumMap;
19import java.util.Enumeration;
20import java.util.HashMap;
21import java.util.Iterator;
22import java.util.List;
23import java.util.Map;
24import java.util.Map.Entry;
25import java.util.SortedMap;
26import java.util.TreeMap;
27import java.util.TreeSet;
28import java.util.function.Predicate;
29import java.util.regex.Pattern;
30import java.util.stream.Collectors;
31
32import javax.swing.JOptionPane;
33import javax.swing.JTree;
34import javax.swing.tree.DefaultMutableTreeNode;
35import javax.swing.tree.TreeModel;
36import javax.swing.tree.TreeNode;
37
38import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
39import org.openstreetmap.josm.data.projection.ProjectionRegistry;
40import org.openstreetmap.josm.data.validation.tests.Addresses;
41import org.openstreetmap.josm.data.validation.tests.ApiCapabilitiesTest;
42import org.openstreetmap.josm.data.validation.tests.BarriersEntrances;
43import org.openstreetmap.josm.data.validation.tests.Coastlines;
44import org.openstreetmap.josm.data.validation.tests.ConditionalKeys;
45import org.openstreetmap.josm.data.validation.tests.CrossingWays;
46import org.openstreetmap.josm.data.validation.tests.DuplicateNode;
47import org.openstreetmap.josm.data.validation.tests.DuplicateRelation;
48import org.openstreetmap.josm.data.validation.tests.DuplicateWay;
49import org.openstreetmap.josm.data.validation.tests.DuplicatedWayNodes;
50import org.openstreetmap.josm.data.validation.tests.Highways;
51import org.openstreetmap.josm.data.validation.tests.InternetTags;
52import org.openstreetmap.josm.data.validation.tests.Lanes;
53import org.openstreetmap.josm.data.validation.tests.LongSegment;
54import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker;
55import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;
56import org.openstreetmap.josm.data.validation.tests.NameMismatch;
57import org.openstreetmap.josm.data.validation.tests.OpeningHourTest;
58import org.openstreetmap.josm.data.validation.tests.OverlappingWays;
59import org.openstreetmap.josm.data.validation.tests.PowerLines;
60import org.openstreetmap.josm.data.validation.tests.PublicTransportRouteTest;
61import org.openstreetmap.josm.data.validation.tests.RelationChecker;
62import org.openstreetmap.josm.data.validation.tests.RightAngleBuildingTest;
63import org.openstreetmap.josm.data.validation.tests.SelfIntersectingWay;
64import org.openstreetmap.josm.data.validation.tests.SharpAngles;
65import org.openstreetmap.josm.data.validation.tests.SimilarNamedWays;
66import org.openstreetmap.josm.data.validation.tests.TagChecker;
67import org.openstreetmap.josm.data.validation.tests.TurnrestrictionTest;
68import org.openstreetmap.josm.data.validation.tests.UnclosedWays;
69import org.openstreetmap.josm.data.validation.tests.UnconnectedWays;
70import org.openstreetmap.josm.data.validation.tests.UntaggedNode;
71import org.openstreetmap.josm.data.validation.tests.UntaggedWay;
72import org.openstreetmap.josm.data.validation.tests.WayConnectedToArea;
73import org.openstreetmap.josm.data.validation.tests.WronglyOrderedWays;
74import org.openstreetmap.josm.gui.MainApplication;
75import org.openstreetmap.josm.gui.layer.ValidatorLayer;
76import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
77import org.openstreetmap.josm.gui.util.GuiHelper;
78import org.openstreetmap.josm.spi.preferences.Config;
79import org.openstreetmap.josm.tools.AlphanumComparator;
80import org.openstreetmap.josm.tools.Logging;
81import org.openstreetmap.josm.tools.Utils;
82
83/**
84 * A OSM data validator.
85 *
86 * @author Francisco R. Santos <frsantos@gmail.com>
87 */
88public final class OsmValidator {
89
90 private OsmValidator() {
91 // Hide default constructor for utilities classes
92 }
93
94 private static volatile ValidatorLayer errorLayer;
95
96 /** Grid detail, multiplier of east,north values for valuable cell sizing */
97 private static double griddetail;
98
99 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 static {
169 for (Class<? extends Test> testClass : CORE_TEST_CLASSES) {
170 addTest(testClass);
171 }
172 }
173
174 /**
175 * Initializes {@code OsmValidator}.
176 */
177 public static void initialize() {
178 initializeGridDetail();
179 loadIgnoredErrors();
180 }
181
182 /**
183 * Returns the validator directory.
184 *
185 * @return The validator directory
186 */
187 public static String getValidatorDir() {
188 File dir = new File(Config.getDirs().getUserDataDirectory(true), "validator");
189 try {
190 return dir.getAbsolutePath();
191 } catch (SecurityException e) {
192 Logging.log(Logging.LEVEL_ERROR, null, e);
193 return dir.getPath();
194 }
195 }
196
197 private static void loadIgnoredErrors() {
198 ignoredErrors.clear();
199 if (ValidatorPrefHelper.PREF_USE_IGNORE.get()) {
200 Config.getPref().getListOfMaps(ValidatorPrefHelper.PREF_IGNORELIST).forEach(ignoredErrors::putAll);
201 Path path = Paths.get(getValidatorDir()).resolve("ignorederrors");
202 try {
203 if (path.toFile().exists()) {
204 try {
205 TreeSet<String> treeSet = new TreeSet<>();
206 treeSet.addAll(Files.readAllLines(path, StandardCharsets.UTF_8));
207 treeSet.forEach(ignore -> ignoredErrors.putIfAbsent(ignore, ""));
208
209 saveIgnoredErrors();
210 Files.deleteIfExists(path);
211
212 } catch (FileNotFoundException e) {
213 Logging.debug(Logging.getErrorMessage(e));
214 } catch (IOException e) {
215 Logging.error(e);
216 }
217 }
218 } catch (SecurityException e) {
219 Logging.log(Logging.LEVEL_ERROR, "Unable to load ignored errors", e);
220 }
221 }
222 }
223
224 /**
225 * Adds an ignored error
226 * @param s The ignore group / sub group name
227 * @see TestError#getIgnoreGroup()
228 * @see TestError#getIgnoreSubGroup()
229 */
230 public static void addIgnoredError(String s) {
231 addIgnoredError(s, "");
232 }
233
234 /**
235 * Adds an ignored error
236 * @param s The ignore group / sub group name
237 * @param description What the error actually is
238 * @see TestError#getIgnoreGroup()
239 * @see TestError#getIgnoreSubGroup()
240 */
241 public static void addIgnoredError(String s, String description) {
242 if (description == null) description = "";
243 ignoredErrors.put(s, description);
244 }
245
246 /**
247 * Make sure that we don't keep single entries for a "group ignore".
248 */
249 static void cleanupIgnoredErrors() {
250 if (ignoredErrors.size() > 1) {
251 List<String> toRemove = new ArrayList<>();
252
253 Iterator<Entry<String, String>> iter = ignoredErrors.entrySet().iterator();
254 String lastKey = iter.next().getKey();
255 while (iter.hasNext()) {
256 String currKey = iter.next().getKey();
257 if (currKey.startsWith(lastKey) && sameCode(currKey, lastKey)) {
258 toRemove.add(currKey);
259 } else {
260 lastKey = currKey;
261 }
262 }
263 toRemove.forEach(ignoredErrors::remove);
264 }
265
266 Map<String, String> tmap = buildIgnore(buildJTreeList());
267 if (!tmap.isEmpty()) {
268 ignoredErrors.clear();
269 ignoredErrors.putAll(tmap);
270 }
271 }
272
273 private static boolean sameCode(String key1, String key2) {
274 return extractCodeFromIgnoreKey(key1).equals(extractCodeFromIgnoreKey(key2));
275 }
276
277 /**
278 * Extract the leading digits building the code for the error key.
279 * @param key the error key
280 * @return the leading digits
281 */
282 private static String extractCodeFromIgnoreKey(String key) {
283 int lenCode = 0;
284
285 for (int i = 0; i < key.length(); i++) {
286 if (key.charAt(i) >= '0' && key.charAt(i) <= '9') {
287 lenCode++;
288 } else {
289 break;
290 }
291 }
292 return key.substring(0, lenCode);
293 }
294
295 /**
296 * Check if a error should be ignored
297 * @param s The ignore group / sub group name
298 * @return <code>true</code> to ignore that error
299 */
300 public static boolean hasIgnoredError(String s) {
301 return ignoredErrors.containsKey(s);
302 }
303
304 /**
305 * Get the list of all ignored errors
306 * @return The <code>Collection&lt;String&gt;</code> of errors that are ignored
307 */
308 public static SortedMap<String, String> getIgnoredErrors() {
309 return ignoredErrors;
310 }
311
312 /**
313 * Build a JTree with a list
314 * @return &lt;type&gt;list as a {@code JTree}
315 */
316 public static JTree buildJTreeList() {
317 DefaultMutableTreeNode root = new DefaultMutableTreeNode(tr("Ignore list"));
318 final Pattern elemId1Pattern = Pattern.compile(":(r|w|n)_");
319 final Pattern elemId2Pattern = Pattern.compile("^[0-9]+$");
320 for (Entry<String, String> e: ignoredErrors.entrySet()) {
321 String key = e.getKey();
322 // key starts with a code, it maybe followed by a string (eg. a MapCSS rule) and
323 // optionally with a list of one or more OSM element IDs
324 String description = e.getValue();
325
326 ArrayList<String> ignoredElementList = new ArrayList<>();
327 String[] osmobjects = elemId1Pattern.split(key);
328 for (int i = 1; i < osmobjects.length; i++) {
329 String osmid = osmobjects[i];
330 if (elemId2Pattern.matcher(osmid).matches()) {
331 osmid = '_' + osmid;
332 int index = key.indexOf(osmid);
333 if (index < key.lastIndexOf(']')) continue;
334 char type = key.charAt(index - 1);
335 ignoredElementList.add(type + osmid);
336 }
337 }
338 for (String osmignore : ignoredElementList) {
339 key = key.replace(':' + osmignore, "");
340 }
341
342 DefaultMutableTreeNode trunk;
343 DefaultMutableTreeNode branch;
344
345 if (description != null && !description.isEmpty()) {
346 trunk = inTree(root, description);
347 branch = inTree(trunk, key);
348 trunk.add(branch);
349 } else {
350 trunk = inTree(root, key);
351 branch = trunk;
352 }
353 if (!ignoredElementList.isEmpty()) {
354 String item;
355 if (ignoredElementList.size() == 1) {
356 item = ignoredElementList.iterator().next();
357 } else {
358 // combination of two or more objects, keep them together
359 item = ignoredElementList.toString(); // [ID1, ID2, ..., IDn]
360 }
361 branch.add(new DefaultMutableTreeNode(item));
362 }
363 root.add(trunk);
364 }
365 return new JTree(root);
366 }
367
368 private static DefaultMutableTreeNode inTree(DefaultMutableTreeNode root, String name) {
369 @SuppressWarnings("unchecked")
370 Enumeration<TreeNode> trunks = root.children();
371 while (trunks.hasMoreElements()) {
372 TreeNode ttrunk = trunks.nextElement();
373 if (ttrunk instanceof DefaultMutableTreeNode) {
374 DefaultMutableTreeNode trunk = (DefaultMutableTreeNode) ttrunk;
375 if (name.equals(trunk.getUserObject())) {
376 return trunk;
377 }
378 }
379 }
380 return new DefaultMutableTreeNode(name);
381 }
382
383 /**
384 * Build a {@code HashMap} from a tree of ignored errors
385 * @param tree The JTree of ignored errors
386 * @return A {@code HashMap} of the ignored errors for comparison
387 */
388 public static Map<String, String> buildIgnore(JTree tree) {
389 TreeModel model = tree.getModel();
390 DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
391 return buildIgnore(model, root);
392 }
393
394 private static Map<String, String> buildIgnore(TreeModel model, DefaultMutableTreeNode node) {
395 HashMap<String, String> rHashMap = new HashMap<>();
396
397 for (int i = 0; i < model.getChildCount(node); i++) {
398 DefaultMutableTreeNode child = (DefaultMutableTreeNode) model.getChild(node, i);
399 if (model.getChildCount(child) == 0) {
400 // create an entry for the error list
401 String key = node.getUserObject().toString();
402 String description;
403
404 if (!model.getRoot().equals(node)) {
405 description = ((DefaultMutableTreeNode) node.getParent()).getUserObject().toString();
406 } else {
407 description = key; // we get here when reading old file ignorederrors
408 }
409 if (tr("Ignore list").equals(description))
410 description = "";
411 if (!key.matches("^[0-9]+(_.*|$)")) {
412 description = key;
413 key = "";
414 }
415
416 String item = child.getUserObject().toString();
417 String entry = null;
418 if (item.matches("^\\[(r|w|n)_.*")) {
419 // list of elements (produced with list.toString() method)
420 entry = key + ":" + item.substring(1, item.lastIndexOf(']')).replace(", ", ":");
421 } else if (item.matches("^(r|w|n)_.*")) {
422 // single element
423 entry = key + ":" + item;
424 } else if (item.matches("^[0-9]+(_.*|)$")) {
425 // no element ids
426 entry = item;
427 }
428 if (entry != null) {
429 rHashMap.put(entry, description);
430 } else {
431 Logging.warn("ignored unexpected item in validator ignore list management dialog:'" + item + "'");
432 }
433 } else {
434 rHashMap.putAll(buildIgnore(model, child));
435 }
436 }
437 return rHashMap;
438 }
439
440 /**
441 * Reset the error list by deleting {@code validator.ignorelist}
442 */
443 public static void resetErrorList() {
444 saveIgnoredErrors();
445 Config.getPref().putListOfMaps(ValidatorPrefHelper.PREF_IGNORELIST, null);
446 OsmValidator.initialize();
447 }
448
449 /**
450 * Saves the names of the ignored errors to a preference
451 */
452 public static void saveIgnoredErrors() {
453 List<Map<String, String>> list = new ArrayList<>();
454 cleanupIgnoredErrors();
455 list.add(ignoredErrors);
456 int i = 0;
457 while (i < list.size()) {
458 if (list.get(i) == null || list.get(i).isEmpty()) {
459 list.remove(i);
460 continue;
461 }
462 i++;
463 }
464 if (list.isEmpty()) list = null;
465 Config.getPref().putListOfMaps(ValidatorPrefHelper.PREF_IGNORELIST, list);
466 }
467
468 /**
469 * Initializes error layer.
470 */
471 public static synchronized void initializeErrorLayer() {
472 if (!ValidatorPrefHelper.PREF_LAYER.get())
473 return;
474 if (errorLayer == null) {
475 errorLayer = new ValidatorLayer();
476 MainApplication.getLayerManager().addLayer(errorLayer);
477 }
478 }
479
480 /**
481 * Resets error layer.
482 * @since 11852
483 */
484 public static synchronized void resetErrorLayer() {
485 errorLayer = null;
486 }
487
488 /**
489 * Gets a map from simple names to all tests.
490 * @return A map of all tests, indexed and sorted by the name of their Java class
491 */
492 public static SortedMap<String, Test> getAllTestsMap() {
493 applyPrefs(allTestsMap, false);
494 applyPrefs(allTestsMap, true);
495 return new TreeMap<>(allTestsMap);
496 }
497
498 /**
499 * Returns the instance of the given test class.
500 * @param <T> testClass type
501 * @param testClass The class of test to retrieve
502 * @return the instance of the given test class, if any, or {@code null}
503 * @since 6670
504 */
505 @SuppressWarnings("unchecked")
506 public static <T extends Test> T getTest(Class<T> testClass) {
507 if (testClass == null) {
508 return null;
509 }
510 return (T) allTestsMap.get(testClass.getName());
511 }
512
513 private static void applyPrefs(Map<String, Test> tests, boolean beforeUpload) {
514 for (String testName : Config.getPref().getList(beforeUpload
515 ? ValidatorPrefHelper.PREF_SKIP_TESTS_BEFORE_UPLOAD : ValidatorPrefHelper.PREF_SKIP_TESTS)) {
516 Test test = tests.get(testName);
517 if (test != null) {
518 if (beforeUpload) {
519 test.testBeforeUpload = false;
520 } else {
521 test.enabled = false;
522 }
523 }
524 }
525 }
526
527 /**
528 * Gets all tests that are possible
529 * @return The tests
530 */
531 public static Collection<Test> getTests() {
532 return getAllTestsMap().values();
533 }
534
535 /**
536 * Gets all tests that are run
537 * @param beforeUpload To get the ones that are run before upload
538 * @return The tests
539 */
540 public static Collection<Test> getEnabledTests(boolean beforeUpload) {
541 Collection<Test> enabledTests = getTests();
542 for (Test t : new ArrayList<>(enabledTests)) {
543 if (beforeUpload ? t.testBeforeUpload : t.enabled) {
544 continue;
545 }
546 enabledTests.remove(t);
547 }
548 return enabledTests;
549 }
550
551 /**
552 * Gets the list of all available test classes
553 *
554 * @return A collection of the test classes
555 */
556 public static Collection<Class<? extends Test>> getAllAvailableTestClasses() {
557 return Collections.unmodifiableCollection(allTests);
558 }
559
560 /**
561 * Initialize grid details based on current projection system. Values based on
562 * the original value fixed for EPSG:4326 (10000) using heuristics (that is, test&amp;error
563 * until most bugs were discovered while keeping the processing time reasonable)
564 */
565 public static void initializeGridDetail() {
566 String code = ProjectionRegistry.getProjection().toCode();
567 if (Arrays.asList(ProjectionPreference.wgs84.allCodes()).contains(code)) {
568 OsmValidator.griddetail = 10_000;
569 } else if (Arrays.asList(ProjectionPreference.mercator.allCodes()).contains(code)) {
570 OsmValidator.griddetail = 0.01;
571 } else if (Arrays.asList(ProjectionPreference.lambert.allCodes()).contains(code)) {
572 OsmValidator.griddetail = 0.1;
573 } else {
574 OsmValidator.griddetail = 1.0;
575 }
576 }
577
578 /**
579 * Returns grid detail, multiplier of east,north values for valuable cell sizing
580 * @return grid detail
581 * @since 11852
582 */
583 public static double getGridDetail() {
584 return griddetail;
585 }
586
587 private static boolean testsInitialized;
588
589 /**
590 * Initializes all tests if this operations hasn't been performed already.
591 */
592 public static synchronized void initializeTests() {
593 if (!testsInitialized) {
594 Logging.debug("Initializing validator tests");
595 final long startTime = System.currentTimeMillis();
596 initializeTests(getTests());
597 testsInitialized = true;
598 if (Logging.isDebugEnabled()) {
599 final long elapsedTime = System.currentTimeMillis() - startTime;
600 Logging.debug("Initializing validator tests completed in {0}", Utils.getDurationString(elapsedTime));
601 }
602 }
603 }
604
605 /**
606 * Initializes all tests
607 * @param allTests The tests to initialize
608 */
609 public static void initializeTests(Collection<? extends Test> allTests) {
610 for (Test test : allTests) {
611 try {
612 if (test.enabled) {
613 test.initialize();
614 }
615 } catch (Exception e) { // NOPMD
616 String message = tr("Error initializing test {0}:\n {1}", test.getClass().getSimpleName(), e);
617 Logging.error(message);
618 if (!GraphicsEnvironment.isHeadless()) {
619 GuiHelper.runInEDT(() ->
620 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), message, tr("Error"), JOptionPane.ERROR_MESSAGE)
621 );
622 }
623 }
624 }
625 }
626
627 /**
628 * Groups the given collection of errors by severity, then message, then description.
629 * @param errors list of errors to group
630 * @param filterToUse optional filter
631 * @return collection of errors grouped by severity, then message, then description
632 * @since 12667
633 */
634 public static Map<Severity, Map<String, Map<String, List<TestError>>>> getErrorsBySeverityMessageDescription(
635 Collection<TestError> errors, Predicate<? super TestError> filterToUse) {
636 return errors.stream().filter(filterToUse).collect(
637 Collectors.groupingBy(TestError::getSeverity, () -> new EnumMap<>(Severity.class),
638 Collectors.groupingBy(TestError::getMessage, () -> new TreeMap<>(AlphanumComparator.getInstance()),
639 Collectors.groupingBy(e -> e.getDescription() == null ? "" : e.getDescription(),
640 () -> new TreeMap<>(AlphanumComparator.getInstance()),
641 Collectors.toList()
642 ))));
643 }
644
645 /**
646 * For unit tests
647 */
648 static void clearIgnoredErrors() {
649 ignoredErrors.clear();
650 }
651}
Note: See TracBrowser for help on using the repository browser.