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

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

checkstyle/pmd

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