001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation; 003 004import java.text.MessageFormat; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.Collection; 008import java.util.Collections; 009import java.util.List; 010import java.util.Locale; 011import java.util.TreeSet; 012import java.util.function.Supplier; 013 014import org.openstreetmap.josm.Main; 015import org.openstreetmap.josm.command.Command; 016import org.openstreetmap.josm.data.osm.Node; 017import org.openstreetmap.josm.data.osm.OsmPrimitive; 018import org.openstreetmap.josm.data.osm.Relation; 019import org.openstreetmap.josm.data.osm.Way; 020import org.openstreetmap.josm.data.osm.WaySegment; 021import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 022import org.openstreetmap.josm.data.osm.event.DataChangedEvent; 023import org.openstreetmap.josm.data.osm.event.DataSetListener; 024import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 025import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; 026import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; 027import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; 028import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; 029import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 030import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor; 031import org.openstreetmap.josm.tools.AlphanumComparator; 032import org.openstreetmap.josm.tools.CheckParameterUtil; 033import org.openstreetmap.josm.tools.I18n; 034 035/** 036 * Validation error 037 * @since 3669 038 */ 039public class TestError implements Comparable<TestError>, DataSetListener { 040 /** is this error on the ignore list */ 041 private boolean ignored; 042 /** Severity */ 043 private Severity severity; 044 /** The error message */ 045 private String message; 046 /** Deeper error description */ 047 private final String description; 048 private final String descriptionEn; 049 /** The affected primitives */ 050 private Collection<? extends OsmPrimitive> primitives; 051 /** The primitives or way segments to be highlighted */ 052 private final Collection<?> highlighted; 053 /** The tester that raised this error */ 054 private Test tester; 055 /** Internal code used by testers to classify errors */ 056 private final int code; 057 /** If this error is selected */ 058 private boolean selected; 059 /** Supplying a command to fix the error */ 060 private final Supplier<Command> fixingCommand; 061 062 /** 063 * A builder for a {@code TestError}. 064 * @since 11129 065 */ 066 public static final class Builder { 067 private final Test tester; 068 private final Severity severity; 069 private final int code; 070 private String message; 071 private String description; 072 private String descriptionEn; 073 private Collection<? extends OsmPrimitive> primitives; 074 private Collection<?> highlighted; 075 private Supplier<Command> fixingCommand; 076 077 Builder(Test tester, Severity severity, int code) { 078 this.tester = tester; 079 this.severity = severity; 080 this.code = code; 081 } 082 083 /** 084 * Sets the error message. 085 * 086 * @param message The error message 087 * @return {@code this} 088 */ 089 public Builder message(String message) { 090 this.message = message; 091 return this; 092 } 093 094 /** 095 * Sets the error message. 096 * 097 * @param message The the message of this error group 098 * @param description The translated description of this error 099 * @param descriptionEn The English description (for ignoring errors) 100 * @return {@code this} 101 */ 102 public Builder messageWithManuallyTranslatedDescription(String message, String description, String descriptionEn) { 103 this.message = message; 104 this.description = description; 105 this.descriptionEn = descriptionEn; 106 return this; 107 } 108 109 /** 110 * Sets the error message. 111 * 112 * @param message The the message of this error group 113 * @param marktrDescription The {@linkplain I18n#marktr prepared for i18n} description of this error 114 * @param args The description arguments to be applied in {@link I18n#tr(String, Object...)} 115 * @return {@code this} 116 */ 117 public Builder message(String message, String marktrDescription, Object... args) { 118 this.message = message; 119 this.description = I18n.tr(marktrDescription, args); 120 this.descriptionEn = new MessageFormat(marktrDescription, Locale.ENGLISH).format(args); 121 return this; 122 } 123 124 /** 125 * Sets the primitives affected by this error. 126 * 127 * @param primitives the primitives affected by this error 128 * @return {@code this} 129 */ 130 public Builder primitives(OsmPrimitive... primitives) { 131 return primitives(Arrays.asList(primitives)); 132 } 133 134 /** 135 * Sets the primitives affected by this error. 136 * 137 * @param primitives the primitives affected by this error 138 * @return {@code this} 139 */ 140 public Builder primitives(Collection<? extends OsmPrimitive> primitives) { 141 CheckParameterUtil.ensureThat(this.primitives == null, "primitives already set"); 142 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives"); 143 this.primitives = primitives; 144 if (this.highlighted == null) { 145 this.highlighted = primitives; 146 } 147 return this; 148 } 149 150 /** 151 * Sets the primitives to highlight when selecting this error. 152 * 153 * @param highlighted the primitives to highlight 154 * @return {@code this} 155 * @see ValidatorVisitor#visit(OsmPrimitive) 156 */ 157 public Builder highlight(OsmPrimitive... highlighted) { 158 return highlight(Arrays.asList(highlighted)); 159 } 160 161 /** 162 * Sets the primitives to highlight when selecting this error. 163 * 164 * @param highlighted the primitives to highlight 165 * @return {@code this} 166 * @see ValidatorVisitor#visit(OsmPrimitive) 167 */ 168 public Builder highlight(Collection<? extends OsmPrimitive> highlighted) { 169 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted"); 170 this.highlighted = highlighted; 171 return this; 172 } 173 174 /** 175 * Sets the way segments to highlight when selecting this error. 176 * 177 * @param highlighted the way segments to highlight 178 * @return {@code this} 179 * @see ValidatorVisitor#visit(WaySegment) 180 */ 181 public Builder highlightWaySegments(Collection<WaySegment> highlighted) { 182 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted"); 183 this.highlighted = highlighted; 184 return this; 185 } 186 187 /** 188 * Sets the node pairs to highlight when selecting this error. 189 * 190 * @param highlighted the node pairs to highlight 191 * @return {@code this} 192 * @see ValidatorVisitor#visit(List) 193 */ 194 public Builder highlightNodePairs(Collection<List<Node>> highlighted) { 195 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted"); 196 this.highlighted = highlighted; 197 return this; 198 } 199 200 /** 201 * Sets a supplier to obtain a command to fix the error. 202 * 203 * @param fixingCommand the fix supplier 204 * @return {@code this} 205 */ 206 public Builder fix(Supplier<Command> fixingCommand) { 207 CheckParameterUtil.ensureThat(this.fixingCommand == null, "fixingCommand already set"); 208 this.fixingCommand = fixingCommand; 209 return this; 210 } 211 212 /** 213 * Returns a new test error with the specified values 214 * 215 * @return a new test error with the specified values 216 * @throws IllegalArgumentException when {@link #message} or {@link #primitives} is null/empty. 217 */ 218 public TestError build() { 219 CheckParameterUtil.ensureParameterNotNull(message, "message not set"); 220 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives not set"); 221 CheckParameterUtil.ensureThat(!primitives.isEmpty(), "primitives is empty"); 222 if (this.highlighted == null) { 223 this.highlighted = Collections.emptySet(); 224 } 225 return new TestError(this); 226 } 227 } 228 229 /** 230 * Starts building a new {@code TestError} 231 * @param tester The tester 232 * @param severity The severity of this error 233 * @param code The test error reference code 234 * @return a new test builder 235 * @since 11129 236 */ 237 public static Builder builder(Test tester, Severity severity, int code) { 238 return new Builder(tester, severity, code); 239 } 240 241 TestError(Builder builder) { 242 this.tester = builder.tester; 243 this.severity = builder.severity; 244 this.message = builder.message; 245 this.description = builder.description; 246 this.descriptionEn = builder.descriptionEn; 247 this.primitives = builder.primitives; 248 this.highlighted = builder.highlighted; 249 this.code = builder.code; 250 this.fixingCommand = builder.fixingCommand; 251 } 252 253 /** 254 * Constructs a new {@code TestError}. 255 * @param tester The tester 256 * @param severity The severity of this error 257 * @param message The error message 258 * @param description The translated description 259 * @param descriptionEn The English description 260 * @param code The test error reference code 261 * @param primitives The affected primitives 262 * @param highlighted OSM primitives to highlight 263 * @deprecated Use {@link #builder} instead. Will be removed in 2016-12. 264 */ 265 @Deprecated 266 public TestError(Test tester, Severity severity, String message, String description, String descriptionEn, 267 int code, Collection<? extends OsmPrimitive> primitives, Collection<?> highlighted) { 268 this.tester = tester; 269 this.severity = severity; 270 this.message = message; 271 this.description = description; 272 this.descriptionEn = descriptionEn; 273 this.primitives = primitives; 274 this.highlighted = highlighted; 275 this.code = code; 276 this.fixingCommand = null; 277 } 278 279 /** 280 * Constructs a new {@code TestError} without description. 281 * @param tester The tester 282 * @param severity The severity of this error 283 * @param message The error message 284 * @param code The test error reference code 285 * @param primitives The affected primitives 286 * @param highlighted OSM primitives to highlight 287 * @deprecated Use {@link #builder} instead. Will be removed in 2016-12. 288 */ 289 @Deprecated 290 public TestError(Test tester, Severity severity, String message, int code, Collection<? extends OsmPrimitive> primitives, 291 Collection<?> highlighted) { 292 this(tester, severity, message, null, null, code, primitives, highlighted); 293 } 294 295 /** 296 * Constructs a new {@code TestError}. 297 * @param tester The tester 298 * @param severity The severity of this error 299 * @param message The error message 300 * @param description The translated description 301 * @param descriptionEn The English description 302 * @param code The test error reference code 303 * @param primitives The affected primitives 304 * @deprecated Use {@link #builder} instead. Will be removed in 2016-12. 305 */ 306 @Deprecated 307 public TestError(Test tester, Severity severity, String message, String description, String descriptionEn, 308 int code, Collection<? extends OsmPrimitive> primitives) { 309 this(tester, severity, message, description, descriptionEn, code, primitives, primitives); 310 } 311 312 /** 313 * Constructs a new {@code TestError} without description. 314 * @param tester The tester 315 * @param severity The severity of this error 316 * @param message The error message 317 * @param code The test error reference code 318 * @param primitives The affected primitives 319 * @deprecated Use {@link #builder} instead. Will be removed in 2016-12. 320 */ 321 @Deprecated 322 public TestError(Test tester, Severity severity, String message, int code, Collection<? extends OsmPrimitive> primitives) { 323 this(tester, severity, message, null, null, code, primitives, primitives); 324 } 325 326 /** 327 * Constructs a new {@code TestError} without description, for a single primitive. 328 * @param tester The tester 329 * @param severity The severity of this error 330 * @param message The error message 331 * @param code The test error reference code 332 * @param primitive The affected primitive 333 * @deprecated Use {@link #builder} instead. Will be removed in 2016-12. 334 */ 335 @Deprecated 336 public TestError(Test tester, Severity severity, String message, int code, OsmPrimitive primitive) { 337 this(tester, severity, message, null, null, code, Collections.singletonList(primitive), Collections 338 .singletonList(primitive)); 339 } 340 341 /** 342 * Constructs a new {@code TestError} for a single primitive. 343 * @param tester The tester 344 * @param severity The severity of this error 345 * @param message The error message 346 * @param description The translated description 347 * @param descriptionEn The English description 348 * @param code The test error reference code 349 * @param primitive The affected primitive 350 * @deprecated Use {@link #builder} instead. Will be removed in 2016-12. 351 */ 352 @Deprecated 353 public TestError(Test tester, Severity severity, String message, String description, String descriptionEn, 354 int code, OsmPrimitive primitive) { 355 this(tester, severity, message, description, descriptionEn, code, Collections.singletonList(primitive)); 356 } 357 358 /** 359 * Gets the error message 360 * @return the error message 361 */ 362 public String getMessage() { 363 return message; 364 } 365 366 /** 367 * Gets the error message 368 * @return the error description 369 */ 370 public String getDescription() { 371 return description; 372 } 373 374 /** 375 * Sets the error message 376 * @param message The error message 377 * @deprecated Use {@link #builder} instead. Will be removed in 2016-12. 378 */ 379 @Deprecated 380 public void setMessage(String message) { 381 this.message = message; 382 } 383 384 /** 385 * Gets the list of primitives affected by this error 386 * @return the list of primitives affected by this error 387 */ 388 public Collection<? extends OsmPrimitive> getPrimitives() { 389 return primitives; 390 } 391 392 /** 393 * Gets the list of primitives affected by this error and are selectable 394 * @return the list of selectable primitives affected by this error 395 */ 396 public Collection<? extends OsmPrimitive> getSelectablePrimitives() { 397 List<OsmPrimitive> selectablePrimitives = new ArrayList<>(primitives.size()); 398 for (OsmPrimitive o : primitives) { 399 if (o.isSelectable()) { 400 selectablePrimitives.add(o); 401 } 402 } 403 return selectablePrimitives; 404 } 405 406 /** 407 * Sets the list of primitives affected by this error 408 * @param primitives the list of primitives affected by this error* 409 * @deprecated Use {@link #builder} instead. Will be removed in 2016-12. 410 */ 411 @Deprecated 412 public void setPrimitives(List<? extends OsmPrimitive> primitives) { 413 this.primitives = primitives; 414 } 415 416 /** 417 * Gets the severity of this error 418 * @return the severity of this error 419 */ 420 public Severity getSeverity() { 421 return severity; 422 } 423 424 /** 425 * Sets the severity of this error 426 * @param severity the severity of this error 427 * @deprecated Use {@link #builder} instead. Will be removed in 2016-12. 428 */ 429 @Deprecated 430 public void setSeverity(Severity severity) { 431 this.severity = severity; 432 } 433 434 /** 435 * Returns the ignore state for this error. 436 * @return the ignore state for this error 437 */ 438 public String getIgnoreState() { 439 Collection<String> strings = new TreeSet<>(); 440 StringBuilder ignorestring = new StringBuilder(getIgnoreSubGroup()); 441 for (OsmPrimitive o : primitives) { 442 // ignore data not yet uploaded 443 if (o.isNew()) 444 return null; 445 String type = "u"; 446 if (o instanceof Way) { 447 type = "w"; 448 } else if (o instanceof Relation) { 449 type = "r"; 450 } else if (o instanceof Node) { 451 type = "n"; 452 } 453 strings.add(type + '_' + o.getId()); 454 } 455 for (String o : strings) { 456 ignorestring.append(':').append(o); 457 } 458 return ignorestring.toString(); 459 } 460 461 public String getIgnoreSubGroup() { 462 String ignorestring = getIgnoreGroup(); 463 if (descriptionEn != null) { 464 ignorestring += '_' + descriptionEn; 465 } 466 return ignorestring; 467 } 468 469 public String getIgnoreGroup() { 470 return Integer.toString(code); 471 } 472 473 public void setIgnored(boolean state) { 474 ignored = state; 475 } 476 477 public boolean isIgnored() { 478 return ignored; 479 } 480 481 /** 482 * Gets the tester that raised this error 483 * @return the tester that raised this error 484 */ 485 public Test getTester() { 486 return tester; 487 } 488 489 /** 490 * Set the tester that raised the error. 491 * @param tester te tester 492 * @deprecated Use {@link #builder} instead. Will be removed in 2016-12. 493 */ 494 @Deprecated 495 public void setTester(Test tester) { 496 this.tester = tester; 497 } 498 499 /** 500 * Gets the code 501 * @return the code 502 */ 503 public int getCode() { 504 return code; 505 } 506 507 /** 508 * Returns true if the error can be fixed automatically 509 * 510 * @return true if the error can be fixed 511 */ 512 public boolean isFixable() { 513 return fixingCommand != null || ((tester != null) && tester.isFixable(this)); 514 } 515 516 /** 517 * Fixes the error with the appropriate command 518 * 519 * @return The command to fix the error 520 */ 521 public Command getFix() { 522 // obtain fix from the error 523 final Command fix = fixingCommand != null ? fixingCommand.get() : null; 524 if (fix != null) { 525 return fix; 526 } 527 528 // obtain fix from the tester 529 if (tester == null || !tester.isFixable(this) || primitives.isEmpty()) 530 return null; 531 532 return tester.fixError(this); 533 } 534 535 /** 536 * Sets the selection flag of this error 537 * @param selected if this error is selected 538 */ 539 public void setSelected(boolean selected) { 540 this.selected = selected; 541 } 542 543 @SuppressWarnings("unchecked") 544 public void visitHighlighted(ValidatorVisitor v) { 545 for (Object o : highlighted) { 546 if (o instanceof OsmPrimitive) { 547 v.visit((OsmPrimitive) o); 548 } else if (o instanceof WaySegment) { 549 v.visit((WaySegment) o); 550 } else if (o instanceof List<?>) { 551 v.visit((List<Node>) o); 552 } 553 } 554 } 555 556 /** 557 * Returns the selection flag of this error 558 * @return true if this error is selected 559 * @since 5671 560 */ 561 public boolean isSelected() { 562 return selected; 563 } 564 565 /** 566 * Returns The primitives or way segments to be highlighted 567 * @return The primitives or way segments to be highlighted 568 * @since 5671 569 */ 570 public Collection<?> getHighlighted() { 571 return highlighted; 572 } 573 574 @Override 575 public int compareTo(TestError o) { 576 if (equals(o)) return 0; 577 578 MultipleNameVisitor v1 = new MultipleNameVisitor(); 579 MultipleNameVisitor v2 = new MultipleNameVisitor(); 580 581 v1.visit(getPrimitives()); 582 v2.visit(o.getPrimitives()); 583 return AlphanumComparator.getInstance().compare(v1.toString(), v2.toString()); 584 } 585 586 @Override public void primitivesRemoved(PrimitivesRemovedEvent event) { 587 // Remove purged primitives (fix #8639) 588 try { 589 primitives.removeAll(event.getPrimitives()); 590 } catch (UnsupportedOperationException e) { 591 if (event.getPrimitives().containsAll(primitives)) { 592 primitives = Collections.emptyList(); 593 } else { 594 Main.warn(e, "Unable to remove primitives from "+this+'.'); 595 } 596 } 597 } 598 599 @Override public void primitivesAdded(PrimitivesAddedEvent event) { 600 // Do nothing 601 } 602 603 @Override public void tagsChanged(TagsChangedEvent event) { 604 // Do nothing 605 } 606 607 @Override public void nodeMoved(NodeMovedEvent event) { 608 // Do nothing 609 } 610 611 @Override public void wayNodesChanged(WayNodesChangedEvent event) { 612 // Do nothing 613 } 614 615 @Override public void relationMembersChanged(RelationMembersChangedEvent event) { 616 // Do nothing 617 } 618 619 @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) { 620 // Do nothing 621 } 622 623 @Override public void dataChanged(DataChangedEvent event) { 624 // Do nothing 625 } 626 627 @Override 628 public String toString() { 629 return "TestError [tester=" + tester + ", code=" + code + ", message=" + message + ']'; 630 } 631}