source: josm/trunk/src/org/openstreetmap/josm/data/validation/TestError.java

Last change on this file was 19112, checked in by stoecker, 22 months ago

javadoc fixes

  • Property svn:eol-style set to native
File size: 23.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation;
3
4import java.awt.geom.Area;
5import java.awt.geom.PathIterator;
6import java.text.MessageFormat;
7import java.time.Instant;
8import java.util.ArrayList;
9import java.util.Arrays;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.List;
13import java.util.Locale;
14import java.util.Map;
15import java.util.Set;
16import java.util.TreeSet;
17import java.util.function.Supplier;
18import java.util.stream.Collectors;
19import java.util.stream.Stream;
20
21import org.openstreetmap.josm.command.Command;
22import org.openstreetmap.josm.data.coor.EastNorth;
23import org.openstreetmap.josm.data.osm.Node;
24import org.openstreetmap.josm.data.osm.OsmPrimitive;
25import org.openstreetmap.josm.data.osm.OsmUtils;
26import org.openstreetmap.josm.data.osm.Relation;
27import org.openstreetmap.josm.data.osm.Way;
28import org.openstreetmap.josm.data.osm.WaySegment;
29import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
30import org.openstreetmap.josm.tools.AlphanumComparator;
31import org.openstreetmap.josm.tools.CheckParameterUtil;
32import org.openstreetmap.josm.tools.I18n;
33
34/**
35 * Validation error
36 * @since 3669
37 */
38public class TestError implements Comparable<TestError> {
39 /**
40 * Used to switch users over to new ignore system, UNIQUE_CODE_MESSAGE_STATE
41 * 1_704_067_200L → 2024-01-01
42 * We can probably remove this and the supporting code in 2025.
43 */
44 private static boolean switchOver = Instant.now().isAfter(Instant.ofEpochMilli(1_704_067_200L));
45 /** is this error on the ignore list */
46 private boolean ignored;
47 /** Severity */
48 private final Severity severity;
49 /** The error message */
50 private final String message;
51 /** Deeper error description */
52 private final String description;
53 private final String descriptionEn;
54 /** The affected primitives */
55 private final Collection<? extends OsmPrimitive> primitives;
56 /** The primitives or way segments to be highlighted */
57 private final Collection<?> highlighted;
58 /** The tester that raised this error */
59 private final Test tester;
60 /** Internal code used by testers to classify errors */
61 private final int code;
62 /** Internal code used by testers to classify errors. Used for moving between JOSM versions. */
63 private final int uniqueCode;
64 /** If this error is selected */
65 private boolean selected;
66 /** If all relevant primitives are known*/
67 private boolean incompletePrimitives;
68 /** Supplying a command to fix the error */
69 private final Supplier<Command> fixingCommand;
70
71 /**
72 * A builder for a {@code TestError}.
73 * @since 11129
74 */
75 public static final class Builder {
76 private final Test tester;
77 private final Severity severity;
78 private final int code;
79 private final int uniqueCode;
80 private String message;
81 private String description;
82 private String descriptionEn;
83 private Collection<? extends OsmPrimitive> primitives;
84 private Collection<?> highlighted;
85 private Supplier<Command> fixingCommand;
86 private boolean incompletePrimitives;
87
88 Builder(Test tester, Severity severity, int code) {
89 this.tester = tester;
90 this.severity = severity;
91 this.code = code;
92 this.uniqueCode = this.tester != null ? this.tester.getClass().getName().hashCode() : code;
93 }
94
95 /**
96 * Sets the error message.
97 *
98 * @param message The error message
99 * @return {@code this}
100 */
101 public Builder message(String message) {
102 this.message = message;
103 return this;
104 }
105
106 /**
107 * Sets the error message.
108 *
109 * @param message The message of this error group
110 * @param description The translated description of this error
111 * @param descriptionEn The English description (for ignoring errors)
112 * @return {@code this}
113 */
114 public Builder messageWithManuallyTranslatedDescription(String message, String description, String descriptionEn) {
115 this.message = message;
116 this.description = description;
117 this.descriptionEn = descriptionEn;
118 return this;
119 }
120
121 /**
122 * Sets the error message.
123 *
124 * @param message The message of this error group
125 * @param marktrDescription The {@linkplain I18n#marktr prepared for i18n} description of this error
126 * @param args The description arguments to be applied in {@link I18n#tr(String, Object...)}
127 * @return {@code this}
128 */
129 public Builder message(String message, String marktrDescription, Object... args) {
130 this.message = message;
131 this.description = I18n.tr(marktrDescription, args);
132 this.descriptionEn = new MessageFormat(marktrDescription, Locale.ENGLISH).format(args);
133 return this;
134 }
135
136 /**
137 * Sets the primitives affected by this error.
138 *
139 * @param primitives the primitives affected by this error
140 * @return {@code this}
141 */
142 public Builder primitives(OsmPrimitive... primitives) {
143 return primitives(Arrays.asList(primitives));
144 }
145
146 /**
147 * Sets the primitives affected by this error.
148 *
149 * @param primitives the primitives affected by this error
150 * @return {@code this}
151 */
152 public Builder primitives(Collection<? extends OsmPrimitive> primitives) {
153 CheckParameterUtil.ensureThat(this.primitives == null, "primitives already set");
154 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives");
155 this.primitives = primitives;
156 if (this.highlighted == null) {
157 this.highlighted = primitives;
158 }
159 return this;
160 }
161
162 /**
163 * Sets the primitives to highlight when selecting this error.
164 *
165 * @param highlighted the primitives to highlight
166 * @return {@code this}
167 * @see ValidatorVisitor#visit(OsmPrimitive)
168 */
169 public Builder highlight(OsmPrimitive... highlighted) {
170 return highlight(Arrays.asList(highlighted));
171 }
172
173 /**
174 * Sets the primitives to highlight when selecting this error.
175 *
176 * @param highlighted the primitives to highlight
177 * @return {@code this}
178 * @see ValidatorVisitor#visit(OsmPrimitive)
179 */
180 public Builder highlight(Collection<? extends OsmPrimitive> highlighted) {
181 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
182 this.highlighted = highlighted;
183 return this;
184 }
185
186 /**
187 * Sets the way segments to highlight when selecting this error.
188 *
189 * @param highlighted the way segments to highlight
190 * @return {@code this}
191 * @see ValidatorVisitor#visit(WaySegment)
192 */
193 public Builder highlightWaySegments(Collection<WaySegment> highlighted) {
194 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
195 this.highlighted = highlighted;
196 return this;
197 }
198
199 /**
200 * Sets the node pairs to highlight when selecting this error.
201 *
202 * @param highlighted the node pairs to highlight
203 * @return {@code this}
204 * @see ValidatorVisitor#visit(List)
205 */
206 public Builder highlightNodePairs(Collection<List<Node>> highlighted) {
207 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
208 this.highlighted = highlighted;
209 return this;
210 }
211
212 /**
213 * Sets an area to highlight when selecting this error.
214 *
215 * @param highlighted the area to highlight
216 * @return {@code this}
217 */
218 public Builder highlight(Area highlighted) {
219 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
220 this.highlighted = Collections.singleton(highlighted);
221 return this;
222 }
223
224 /**
225 * Sets a flag that the list of primitives may be incomplete. See #23397
226 *
227 * @return {@code this}
228 */
229 public Builder imcompletePrimitives() {
230 this.incompletePrimitives = true;
231 return this;
232 }
233
234 /**
235 * Sets a supplier to obtain a command to fix the error.
236 *
237 * @param fixingCommand the fix supplier. Can be null
238 * @return {@code this}
239 */
240 public Builder fix(Supplier<Command> fixingCommand) {
241 CheckParameterUtil.ensureThat(this.fixingCommand == null, "fixingCommand already set");
242 this.fixingCommand = fixingCommand;
243 return this;
244 }
245
246 /**
247 * Returns a new test error with the specified values
248 *
249 * @return a new test error with the specified values
250 * @throws IllegalArgumentException when {@link #message} or {@link #primitives} is null.
251 */
252 public TestError build() {
253 CheckParameterUtil.ensureParameterNotNull(message, "message not set");
254 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives not set");
255 if (this.highlighted == null) {
256 this.highlighted = Collections.emptySet();
257 }
258 return new TestError(this);
259 }
260 }
261
262 /**
263 * Update error codes on read and save. Used for tests.
264 * @param updateErrorCodes {@code true} to update error codes. See {@link #switchOver} for default.
265 */
266 static void setUpdateErrorCodes(boolean updateErrorCodes) {
267 switchOver = updateErrorCodes;
268 }
269
270 /**
271 * Starts building a new {@code TestError}
272 * @param tester The tester
273 * @param severity The severity of this error
274 * @param code The test error reference code
275 * @return a new test builder
276 * @since 11129
277 */
278 public static Builder builder(Test tester, Severity severity, int code) {
279 return new Builder(tester, severity, code);
280 }
281
282 TestError(Builder builder) {
283 this.tester = builder.tester;
284 this.severity = builder.severity;
285 this.message = builder.message;
286 this.description = builder.description;
287 this.descriptionEn = builder.descriptionEn;
288 this.primitives = builder.primitives;
289 this.highlighted = builder.highlighted;
290 this.code = builder.code;
291 this.uniqueCode = builder.uniqueCode;
292 this.fixingCommand = builder.fixingCommand;
293 this.incompletePrimitives = builder.incompletePrimitives;
294 }
295
296 /**
297 * Gets the error message
298 * @return the error message
299 */
300 public String getMessage() {
301 return message;
302 }
303
304 /**
305 * Gets the error message
306 * @return the error description
307 */
308 public String getDescription() {
309 return description;
310 }
311
312 /**
313 * Gets the list of primitives affected by this error
314 * @return the list of primitives affected by this error
315 */
316 public Collection<? extends OsmPrimitive> getPrimitives() {
317 return Collections.unmodifiableCollection(primitives);
318 }
319
320 /**
321 * Gets all primitives of the given type affected by this error
322 * @param type restrict primitives to subclasses
323 * @param <T> type of primitives
324 * @return the primitives as Stream
325 */
326 public final <T extends OsmPrimitive> Stream<T> primitives(Class<T> type) {
327 return primitives.stream()
328 .filter(type::isInstance)
329 .map(type::cast);
330 }
331
332 /**
333 * Gets the severity of this error
334 * @return the severity of this error
335 */
336 public Severity getSeverity() {
337 return severity;
338 }
339
340 /**
341 * Returns the ignore state for this error.
342 * @return the ignore state for this error or null if any primitive is new
343 */
344 public String getIgnoreState() {
345 return getIgnoreState(false);
346 }
347
348 /**
349 * Get the ignore state
350 * @param useOriginal if {@code true}, use the original code to get the ignore state
351 * @return The ignore state ({@link #getIgnoreGroup} + ignored object list)
352 */
353 private String getIgnoreState(boolean useOriginal) {
354 Collection<String> strings = new TreeSet<>();
355 for (OsmPrimitive o : primitives) {
356 // ignore data not yet uploaded
357 if (o.isNew())
358 return null;
359 String type = "u";
360 if (o instanceof Way) {
361 type = "w";
362 } else if (o instanceof Relation) {
363 type = "r";
364 } else if (o instanceof Node) {
365 type = "n";
366 }
367 strings.add(type + '_' + o.getId());
368 }
369 return strings.stream().map(o -> ':' + o).collect(Collectors.joining("", getIgnoreSubGroup(useOriginal), ""));
370 }
371
372 /**
373 * Check if this error matches an entry in the ignore list and
374 * set the ignored flag if it is.
375 * @return the new ignored state
376 */
377 public boolean updateIgnored() {
378 setIgnored(calcIgnored());
379 return isIgnored();
380 }
381
382 private boolean calcIgnored() {
383 // Begin code removal section (backwards compatibility)
384 if (OsmValidator.hasIgnoredError(getIgnoreGroup(true))) {
385 updateIgnoreList(getIgnoreGroup(true), getIgnoreGroup(false));
386 return true;
387 }
388 if (OsmValidator.hasIgnoredError(getIgnoreSubGroup(true))) {
389 updateIgnoreList(getIgnoreSubGroup(true), getIgnoreSubGroup(false));
390 return true;
391 }
392 String oldState = getIgnoreState(true);
393 String state = getIgnoreState(false);
394 if (oldState != null && OsmValidator.hasIgnoredError(oldState)) {
395 updateIgnoreList(oldState, state);
396 return true;
397 }
398 // End code removal section
399 if (OsmValidator.hasIgnoredError(getIgnoreGroup()))
400 return true;
401 if (OsmValidator.hasIgnoredError(getIgnoreSubGroup()))
402 return true;
403 return state != null && OsmValidator.hasIgnoredError(state);
404 }
405
406 /**
407 * Convert old keys to new keys. Only takes effect when {@link #switchOver} is true
408 * @param oldKey The key to replace
409 * @param newKey The new key
410 */
411 private static void updateIgnoreList(String oldKey, String newKey) {
412 if (switchOver) {
413 Map<String, String> errors = OsmValidator.getIgnoredErrors();
414 if (errors.containsKey(oldKey)) {
415 String value = errors.remove(oldKey);
416 errors.put(newKey, value);
417 }
418 }
419 }
420
421 /**
422 * Gets the ignores subgroup that is more specialized than {@link #getIgnoreGroup()}
423 * @return The ignore sub group
424 */
425 public String getIgnoreSubGroup() {
426 return getIgnoreSubGroup(false);
427 }
428
429 /**
430 * Get the subgroup for the error
431 * @param useOriginal if {@code true}, use the original code instead of the new unique codes.
432 * @return The ignore subgroup
433 */
434 private String getIgnoreSubGroup(boolean useOriginal) {
435 if (code == 3000) {
436 // see #19053
437 return "3000_" + (description == null ? message : description);
438 }
439 String ignorestring = getIgnoreGroup(useOriginal);
440 if (descriptionEn != null) {
441 ignorestring += '_' + descriptionEn;
442 }
443 return ignorestring;
444 }
445
446 /**
447 * Gets the ignore group ID that is used to allow the user to ignore all same errors
448 * @return The group id
449 * @see TestError#getIgnoreSubGroup()
450 */
451 public String getIgnoreGroup() {
452 return getIgnoreGroup(false);
453 }
454
455 /**
456 * Get the ignore group
457 * @param useOriginal if {@code true}, use the original code instead of a unique code + original code.
458 * Used for reading and understanding old ignore groups.
459 * @return The ignore group.
460 */
461 private String getIgnoreGroup(boolean useOriginal) {
462 if (code == 3000) {
463 // see #19053
464 return "3000_" + getMessage();
465 }
466 if (useOriginal) {
467 return Integer.toString(this.code);
468 }
469 return this.uniqueCode + "_" + this.code;
470 }
471
472 /**
473 * Flags this error as ignored
474 * @param state The ignore flag
475 */
476 public void setIgnored(boolean state) {
477 ignored = state;
478 }
479
480 /**
481 * Checks if this error is ignored
482 * @return <code>true</code> if it is ignored
483 */
484 public boolean isIgnored() {
485 return ignored;
486 }
487
488 /**
489 * Gets the tester that raised this error
490 * @return the tester that raised this error
491 */
492 public Test getTester() {
493 return tester;
494 }
495
496 /**
497 * Gets the code
498 * @return the code
499 */
500 public int getCode() {
501 return code;
502 }
503
504 /**
505 * Get the unique code for this test. Used for ignore lists.
506 * @return The unique code (generated with {@code tester.getClass().getName().hashCode() + code}).
507 * @since 18636
508 */
509 public int getUniqueCode() {
510 return this.uniqueCode;
511 }
512
513 /**
514 * Returns true if the error can be fixed automatically
515 *
516 * @return true if the error can be fixed
517 */
518 public boolean isFixable() {
519 return (fixingCommand != null || ((tester != null) && tester.isFixable(this)))
520 && OsmUtils.isOsmCollectionEditable(primitives);
521 }
522
523 /**
524 * Fixes the error with the appropriate command
525 *
526 * @return The command to fix the error
527 */
528 public Command getFix() {
529 // obtain fix from the error
530 final Command fix = fixingCommand != null ? fixingCommand.get() : null;
531 if (fix != null) {
532 return fix;
533 }
534
535 // obtain fix from the tester
536 if (tester == null || !tester.isFixable(this) || primitives.isEmpty())
537 return null;
538
539 return tester.fixError(this);
540 }
541
542 /**
543 * Sets the selection flag of this error
544 * @param selected if this error is selected
545 */
546 public void setSelected(boolean selected) {
547 this.selected = selected;
548 }
549
550 /**
551 * Visits all highlighted validation elements
552 * @param v The visitor that should receive a visit-notification on all highlighted elements
553 */
554 @SuppressWarnings("unchecked")
555 public void visitHighlighted(ValidatorVisitor v) {
556 for (Object o : highlighted) {
557 if (o instanceof OsmPrimitive) {
558 v.visit((OsmPrimitive) o);
559 } else if (o instanceof WaySegment) {
560 v.visit((WaySegment) o);
561 } else if (o instanceof List<?>) {
562 v.visit((List<Node>) o);
563 } else if (o instanceof Area) {
564 for (List<Node> l : getHiliteNodesForArea((Area) o)) {
565 v.visit(l);
566 }
567 }
568 }
569 }
570
571 /**
572 * Calculate list of node pairs describing the area.
573 * @param area the area
574 * @return list of node pairs describing the area
575 */
576 private static List<List<Node>> getHiliteNodesForArea(Area area) {
577 List<List<Node>> hilite = new ArrayList<>();
578 PathIterator pit = area.getPathIterator(null);
579 double[] res = new double[6];
580 List<Node> nodes = new ArrayList<>();
581 while (!pit.isDone()) {
582 int type = pit.currentSegment(res);
583 Node n = new Node(new EastNorth(res[0], res[1]));
584 switch (type) {
585 case PathIterator.SEG_MOVETO:
586 if (!nodes.isEmpty()) {
587 hilite.add(nodes);
588 }
589 nodes = new ArrayList<>();
590 nodes.add(n);
591 break;
592 case PathIterator.SEG_LINETO:
593 nodes.add(n);
594 break;
595 case PathIterator.SEG_CLOSE:
596 if (!nodes.isEmpty()) {
597 nodes.add(nodes.get(0));
598 hilite.add(nodes);
599 nodes = new ArrayList<>();
600 }
601 break;
602 default:
603 break;
604 }
605 pit.next();
606 }
607 if (nodes.size() > 1) {
608 hilite.add(nodes);
609 }
610 return hilite;
611 }
612
613 /**
614 * Returns the selection flag of this error
615 * @return true if this error is selected
616 * @since 5671
617 */
618 public boolean isSelected() {
619 return selected;
620 }
621
622 /**
623 * Returns The primitives or way segments to be highlighted
624 * @return The primitives or way segments to be highlighted
625 * @since 5671
626 */
627 public Collection<?> getHighlighted() {
628 return Collections.unmodifiableCollection(highlighted);
629 }
630
631 @Override
632 public int compareTo(TestError o) {
633 if (equals(o)) return 0;
634
635 return AlphanumComparator.getInstance().compare(getNameVisitor().toString(), o.getNameVisitor().toString());
636 }
637
638 /**
639 * Returns a new {@link MultipleNameVisitor} for the list of primitives affected by this error.
640 * @return Name visitor (used in cell renderer and for sorting)
641 */
642 public MultipleNameVisitor getNameVisitor() {
643 MultipleNameVisitor v = new MultipleNameVisitor();
644 v.visit(getPrimitives());
645 return v;
646 }
647
648 /**
649 * Tests if two errors are similar, i.e.,
650 * same code and description and same combination of primitives and same combination of highlighted objects, but maybe with different orders.
651 * @param other the other error to be compared
652 * @return true if two errors are similar
653 */
654 public boolean isSimilar(TestError other) {
655 return getUniqueCode() == other.getUniqueCode()
656 && getCode() == other.getCode()
657 && getMessage().equals(other.getMessage())
658 && getPrimitives().size() == other.getPrimitives().size()
659 && getPrimitives().containsAll(other.getPrimitives())
660 && highlightedIsEqual(getHighlighted(), other.getHighlighted());
661 }
662
663 private static boolean highlightedIsEqual(Collection<?> highlighted, Collection<?> highlighted2) {
664 if (highlighted.size() == highlighted2.size()) {
665 if (!highlighted.isEmpty()) {
666 Object h1 = highlighted.iterator().next();
667 Object h2 = highlighted2.iterator().next();
668 if (h1 instanceof Area && h2 instanceof Area) {
669 return ((Area) h1).equals((Area) h2);
670 }
671 return highlighted.containsAll(highlighted2);
672 }
673 return true;
674 }
675 return false;
676 }
677
678 @Override
679 public String toString() {
680 return "TestError [tester=" + tester + ", unique code=" + this.uniqueCode +
681 ", code=" + code + ", message=" + message + ']';
682 }
683
684 /**
685 * Check if any of the primitives in this error occurs in the given set of primitives.
686 * @param given the set of primitives
687 * @return true if any of the primitives in this error occurs in the given set of primitives, else false
688 * @since 18960
689 */
690 public boolean isConcerned(Set<? extends OsmPrimitive> given) {
691 if (incompletePrimitives)
692 return true;
693 for (OsmPrimitive p : getPrimitives()) {
694 if (given.contains(p)) {
695 return true;
696 }
697 }
698 return false;
699 }
700}
Note: See TracBrowser for help on using the repository browser.