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 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 278 */ 279 public String getIgnoreState() { 280 Collection<String> strings = new TreeSet<>(); 281 StringBuilder ignorestring = new StringBuilder(getIgnoreSubGroup()); 282 for (OsmPrimitive o : primitives) { 283 // ignore data not yet uploaded 284 if (o.isNew()) 285 return null; 286 String type = "u"; 287 if (o instanceof Way) { 288 type = "w"; 289 } else if (o instanceof Relation) { 290 type = "r"; 291 } else if (o instanceof Node) { 292 type = "n"; 293 } 294 strings.add(type + '_' + o.getId()); 295 } 296 for (String o : strings) { 297 ignorestring.append(':').append(o); 298 } 299 return ignorestring.toString(); 300 } 301 302 /** 303 * Gets the ignores subgroup that is more specialized than {@link #getIgnoreGroup()} 304 * @return The ignore sub group 305 */ 306 public String getIgnoreSubGroup() { 307 String ignorestring = getIgnoreGroup(); 308 if (descriptionEn != null) { 309 ignorestring += '_' + descriptionEn; 310 } 311 return ignorestring; 312 } 313 314 /** 315 * Gets the ignore group ID that is used to allow the user to ignore all same errors 316 * @return The group id 317 * @see TestError#getIgnoreSubGroup() 318 */ 319 public String getIgnoreGroup() { 320 return Integer.toString(code); 321 } 322 323 /** 324 * Flags this error as ignored 325 * @param state The ignore flag 326 */ 327 public void setIgnored(boolean state) { 328 ignored = state; 329 } 330 331 /** 332 * Checks if this error is ignored 333 * @return <code>true</code> if it is ignored 334 */ 335 public boolean isIgnored() { 336 return ignored; 337 } 338 339 /** 340 * Gets the tester that raised this error 341 * @return the tester that raised this error 342 */ 343 public Test getTester() { 344 return tester; 345 } 346 347 /** 348 * Gets the code 349 * @return the code 350 */ 351 public int getCode() { 352 return code; 353 } 354 355 /** 356 * Returns true if the error can be fixed automatically 357 * 358 * @return true if the error can be fixed 359 */ 360 public boolean isFixable() { 361 return (fixingCommand != null || ((tester != null) && tester.isFixable(this))) 362 && OsmUtils.isOsmCollectionEditable(primitives); 363 } 364 365 /** 366 * Fixes the error with the appropriate command 367 * 368 * @return The command to fix the error 369 */ 370 public Command getFix() { 371 // obtain fix from the error 372 final Command fix = fixingCommand != null ? fixingCommand.get() : null; 373 if (fix != null) { 374 return fix; 375 } 376 377 // obtain fix from the tester 378 if (tester == null || !tester.isFixable(this) || primitives.isEmpty()) 379 return null; 380 381 return tester.fixError(this); 382 } 383 384 /** 385 * Sets the selection flag of this error 386 * @param selected if this error is selected 387 */ 388 public void setSelected(boolean selected) { 389 this.selected = selected; 390 } 391 392 /** 393 * Visits all highlighted validation elements 394 * @param v The visitor that should receive a visit-notification on all highlighted elements 395 */ 396 @SuppressWarnings("unchecked") 397 public void visitHighlighted(ValidatorVisitor v) { 398 for (Object o : highlighted) { 399 if (o instanceof OsmPrimitive) { 400 v.visit((OsmPrimitive) o); 401 } else if (o instanceof WaySegment) { 402 v.visit((WaySegment) o); 403 } else if (o instanceof List<?>) { 404 v.visit((List<Node>) o); 405 } 406 } 407 } 408 409 /** 410 * Returns the selection flag of this error 411 * @return true if this error is selected 412 * @since 5671 413 */ 414 public boolean isSelected() { 415 return selected; 416 } 417 418 /** 419 * Returns The primitives or way segments to be highlighted 420 * @return The primitives or way segments to be highlighted 421 * @since 5671 422 */ 423 public Collection<?> getHighlighted() { 424 return Collections.unmodifiableCollection(highlighted); 425 } 426 427 @Override 428 public int compareTo(TestError o) { 429 if (equals(o)) return 0; 430 431 MultipleNameVisitor v1 = new MultipleNameVisitor(); 432 MultipleNameVisitor v2 = new MultipleNameVisitor(); 433 434 v1.visit(getPrimitives()); 435 v2.visit(o.getPrimitives()); 436 return AlphanumComparator.getInstance().compare(v1.toString(), v2.toString()); 437 } 438 439 @Override 440 public String toString() { 441 return "TestError [tester=" + tester + ", code=" + code + ", message=" + message + ']'; 442 } 443}