001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation; 003 004import java.text.MessageFormat; 005import java.util.Arrays; 006import java.util.Collection; 007import java.util.Collections; 008import java.util.List; 009import java.util.Locale; 010import java.util.TreeSet; 011import java.util.function.Supplier; 012 013import org.openstreetmap.josm.command.Command; 014import org.openstreetmap.josm.data.osm.Node; 015import org.openstreetmap.josm.data.osm.OsmPrimitive; 016import org.openstreetmap.josm.data.osm.OsmUtils; 017import org.openstreetmap.josm.data.osm.Relation; 018import org.openstreetmap.josm.data.osm.Way; 019import org.openstreetmap.josm.data.osm.WaySegment; 020import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor; 021import org.openstreetmap.josm.tools.AlphanumComparator; 022import org.openstreetmap.josm.tools.CheckParameterUtil; 023import org.openstreetmap.josm.tools.I18n; 024 025/** 026 * Validation error 027 * @since 3669 028 */ 029public class TestError implements Comparable<TestError> { 030 /** is this error on the ignore list */ 031 private boolean ignored; 032 /** Severity */ 033 private final Severity severity; 034 /** The error message */ 035 private final String message; 036 /** Deeper error description */ 037 private final String description; 038 private final String descriptionEn; 039 /** The affected primitives */ 040 private final Collection<? extends OsmPrimitive> primitives; 041 /** The primitives or way segments to be highlighted */ 042 private final Collection<?> highlighted; 043 /** The tester that raised this error */ 044 private final Test tester; 045 /** Internal code used by testers to classify errors */ 046 private final int code; 047 /** If this error is selected */ 048 private boolean selected; 049 /** Supplying a command to fix the error */ 050 private final Supplier<Command> fixingCommand; 051 052 /** 053 * A builder for a {@code TestError}. 054 * @since 11129 055 */ 056 public static final class Builder { 057 private final Test tester; 058 private final Severity severity; 059 private final int code; 060 private String message; 061 private String description; 062 private String descriptionEn; 063 private Collection<? extends OsmPrimitive> primitives; 064 private Collection<?> highlighted; 065 private Supplier<Command> fixingCommand; 066 067 Builder(Test tester, Severity severity, int code) { 068 this.tester = tester; 069 this.severity = severity; 070 this.code = code; 071 } 072 073 /** 074 * Sets the error message. 075 * 076 * @param message The error message 077 * @return {@code this} 078 */ 079 public Builder message(String message) { 080 this.message = message; 081 return this; 082 } 083 084 /** 085 * Sets the error message. 086 * 087 * @param message The message of this error group 088 * @param description The translated description of this error 089 * @param descriptionEn The English description (for ignoring errors) 090 * @return {@code this} 091 */ 092 public Builder messageWithManuallyTranslatedDescription(String message, String description, String descriptionEn) { 093 this.message = message; 094 this.description = description; 095 this.descriptionEn = descriptionEn; 096 return this; 097 } 098 099 /** 100 * Sets the error message. 101 * 102 * @param message The the message of this error group 103 * @param marktrDescription The {@linkplain I18n#marktr prepared for i18n} description of this error 104 * @param args The description arguments to be applied in {@link I18n#tr(String, Object...)} 105 * @return {@code this} 106 */ 107 public Builder message(String message, String marktrDescription, Object... args) { 108 this.message = message; 109 this.description = I18n.tr(marktrDescription, args); 110 this.descriptionEn = new MessageFormat(marktrDescription, Locale.ENGLISH).format(args); 111 return this; 112 } 113 114 /** 115 * Sets the primitives affected by this error. 116 * 117 * @param primitives the primitives affected by this error 118 * @return {@code this} 119 */ 120 public Builder primitives(OsmPrimitive... primitives) { 121 return primitives(Arrays.asList(primitives)); 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(Collection<? extends OsmPrimitive> primitives) { 131 CheckParameterUtil.ensureThat(this.primitives == null, "primitives already set"); 132 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives"); 133 this.primitives = primitives; 134 if (this.highlighted == null) { 135 this.highlighted = primitives; 136 } 137 return this; 138 } 139 140 /** 141 * Sets the primitives to highlight when selecting this error. 142 * 143 * @param highlighted the primitives to highlight 144 * @return {@code this} 145 * @see ValidatorVisitor#visit(OsmPrimitive) 146 */ 147 public Builder highlight(OsmPrimitive... highlighted) { 148 return highlight(Arrays.asList(highlighted)); 149 } 150 151 /** 152 * Sets the primitives to highlight when selecting this error. 153 * 154 * @param highlighted the primitives to highlight 155 * @return {@code this} 156 * @see ValidatorVisitor#visit(OsmPrimitive) 157 */ 158 public Builder highlight(Collection<? extends OsmPrimitive> highlighted) { 159 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted"); 160 this.highlighted = highlighted; 161 return this; 162 } 163 164 /** 165 * Sets the way segments to highlight when selecting this error. 166 * 167 * @param highlighted the way segments to highlight 168 * @return {@code this} 169 * @see ValidatorVisitor#visit(WaySegment) 170 */ 171 public Builder highlightWaySegments(Collection<WaySegment> highlighted) { 172 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted"); 173 this.highlighted = highlighted; 174 return this; 175 } 176 177 /** 178 * Sets the node pairs to highlight when selecting this error. 179 * 180 * @param highlighted the node pairs to highlight 181 * @return {@code this} 182 * @see ValidatorVisitor#visit(List) 183 */ 184 public Builder highlightNodePairs(Collection<List<Node>> highlighted) { 185 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted"); 186 this.highlighted = highlighted; 187 return this; 188 } 189 190 /** 191 * Sets a supplier to obtain a command to fix the error. 192 * 193 * @param fixingCommand the fix supplier. Can be null 194 * @return {@code this} 195 */ 196 public Builder fix(Supplier<Command> fixingCommand) { 197 CheckParameterUtil.ensureThat(this.fixingCommand == null, "fixingCommand already set"); 198 this.fixingCommand = fixingCommand; 199 return this; 200 } 201 202 /** 203 * Returns a new test error with the specified values 204 * 205 * @return a new test error with the specified values 206 * @throws IllegalArgumentException when {@link #message} or {@link #primitives} is null/empty. 207 */ 208 public TestError build() { 209 CheckParameterUtil.ensureParameterNotNull(message, "message not set"); 210 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives not set"); 211 CheckParameterUtil.ensureThat(!primitives.isEmpty(), "primitives is empty"); 212 if (this.highlighted == null) { 213 this.highlighted = Collections.emptySet(); 214 } 215 return new TestError(this); 216 } 217 } 218 219 /** 220 * Starts building a new {@code TestError} 221 * @param tester The tester 222 * @param severity The severity of this error 223 * @param code The test error reference code 224 * @return a new test builder 225 * @since 11129 226 */ 227 public static Builder builder(Test tester, Severity severity, int code) { 228 return new Builder(tester, severity, code); 229 } 230 231 TestError(Builder builder) { 232 this.tester = builder.tester; 233 this.severity = builder.severity; 234 this.message = builder.message; 235 this.description = builder.description; 236 this.descriptionEn = builder.descriptionEn; 237 this.primitives = builder.primitives; 238 this.highlighted = builder.highlighted; 239 this.code = builder.code; 240 this.fixingCommand = builder.fixingCommand; 241 } 242 243 /** 244 * Gets the error message 245 * @return the error message 246 */ 247 public String getMessage() { 248 return message; 249 } 250 251 /** 252 * Gets the error message 253 * @return the error description 254 */ 255 public String getDescription() { 256 return description; 257 } 258 259 /** 260 * Gets the list of primitives affected by this error 261 * @return the list of primitives affected by this error 262 */ 263 public Collection<? extends OsmPrimitive> getPrimitives() { 264 return Collections.unmodifiableCollection(primitives); 265 } 266 267 /** 268 * Gets the severity of this error 269 * @return the severity of this error 270 */ 271 public Severity getSeverity() { 272 return severity; 273 } 274 275 /** 276 * Returns the ignore state for this error. 277 * @return the ignore state for this error or null if any primitive is new 278 */ 279 public String getIgnoreState() { 280 Collection<String> strings = new TreeSet<>(); 281 for (OsmPrimitive o : primitives) { 282 // ignore data not yet uploaded 283 if (o.isNew()) 284 return null; 285 String type = "u"; 286 if (o instanceof Way) { 287 type = "w"; 288 } else if (o instanceof Relation) { 289 type = "r"; 290 } else if (o instanceof Node) { 291 type = "n"; 292 } 293 strings.add(type + '_' + o.getId()); 294 } 295 StringBuilder ignorestring = new StringBuilder(getIgnoreSubGroup()); 296 for (String o : strings) { 297 ignorestring.append(':').append(o); 298 } 299 return ignorestring.toString(); 300 } 301 302 /** 303 * Check if this error matches an entry in the ignore list and 304 * set the ignored flag if it is. 305 * @return the new ignored state 306 */ 307 public boolean updateIgnored() { 308 setIgnored(calcIgnored()); 309 return isIgnored(); 310 } 311 312 private boolean calcIgnored() { 313 if (OsmValidator.hasIgnoredError(getIgnoreGroup())) 314 return true; 315 if (OsmValidator.hasIgnoredError(getIgnoreSubGroup())) 316 return true; 317 String state = getIgnoreState(); 318 return state != null && OsmValidator.hasIgnoredError(state); 319 } 320 321 /** 322 * Gets the ignores subgroup that is more specialized than {@link #getIgnoreGroup()} 323 * @return The ignore sub group 324 */ 325 public String getIgnoreSubGroup() { 326 String ignorestring = getIgnoreGroup(); 327 if (descriptionEn != null) { 328 ignorestring += '_' + descriptionEn; 329 } 330 return ignorestring; 331 } 332 333 /** 334 * Gets the ignore group ID that is used to allow the user to ignore all same errors 335 * @return The group id 336 * @see TestError#getIgnoreSubGroup() 337 */ 338 public String getIgnoreGroup() { 339 return Integer.toString(code); 340 } 341 342 /** 343 * Flags this error as ignored 344 * @param state The ignore flag 345 */ 346 public void setIgnored(boolean state) { 347 ignored = state; 348 } 349 350 /** 351 * Checks if this error is ignored 352 * @return <code>true</code> if it is ignored 353 */ 354 public boolean isIgnored() { 355 return ignored; 356 } 357 358 /** 359 * Gets the tester that raised this error 360 * @return the tester that raised this error 361 */ 362 public Test getTester() { 363 return tester; 364 } 365 366 /** 367 * Gets the code 368 * @return the code 369 */ 370 public int getCode() { 371 return code; 372 } 373 374 /** 375 * Returns true if the error can be fixed automatically 376 * 377 * @return true if the error can be fixed 378 */ 379 public boolean isFixable() { 380 return (fixingCommand != null || ((tester != null) && tester.isFixable(this))) 381 && OsmUtils.isOsmCollectionEditable(primitives); 382 } 383 384 /** 385 * Fixes the error with the appropriate command 386 * 387 * @return The command to fix the error 388 */ 389 public Command getFix() { 390 // obtain fix from the error 391 final Command fix = fixingCommand != null ? fixingCommand.get() : null; 392 if (fix != null) { 393 return fix; 394 } 395 396 // obtain fix from the tester 397 if (tester == null || !tester.isFixable(this) || primitives.isEmpty()) 398 return null; 399 400 return tester.fixError(this); 401 } 402 403 /** 404 * Sets the selection flag of this error 405 * @param selected if this error is selected 406 */ 407 public void setSelected(boolean selected) { 408 this.selected = selected; 409 } 410 411 /** 412 * Visits all highlighted validation elements 413 * @param v The visitor that should receive a visit-notification on all highlighted elements 414 */ 415 @SuppressWarnings("unchecked") 416 public void visitHighlighted(ValidatorVisitor v) { 417 for (Object o : highlighted) { 418 if (o instanceof OsmPrimitive) { 419 v.visit((OsmPrimitive) o); 420 } else if (o instanceof WaySegment) { 421 v.visit((WaySegment) o); 422 } else if (o instanceof List<?>) { 423 v.visit((List<Node>) o); 424 } 425 } 426 } 427 428 /** 429 * Returns the selection flag of this error 430 * @return true if this error is selected 431 * @since 5671 432 */ 433 public boolean isSelected() { 434 return selected; 435 } 436 437 /** 438 * Returns The primitives or way segments to be highlighted 439 * @return The primitives or way segments to be highlighted 440 * @since 5671 441 */ 442 public Collection<?> getHighlighted() { 443 return Collections.unmodifiableCollection(highlighted); 444 } 445 446 @Override 447 public int compareTo(TestError o) { 448 if (equals(o)) return 0; 449 450 return AlphanumComparator.getInstance().compare(getNameVisitor().toString(), o.getNameVisitor().toString()); 451 } 452 453 /** 454 * @return Name visitor (used in cell renderer and for sorting) 455 */ 456 public MultipleNameVisitor getNameVisitor() { 457 MultipleNameVisitor v = new MultipleNameVisitor(); 458 v.visit(getPrimitives()); 459 return v; 460 } 461 462 @Override 463 public String toString() { 464 return "TestError [tester=" + tester + ", code=" + code + ", message=" + message + ']'; 465 } 466}