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.Relation; 017import org.openstreetmap.josm.data.osm.Way; 018import org.openstreetmap.josm.data.osm.WaySegment; 019import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor; 020import org.openstreetmap.josm.tools.AlphanumComparator; 021import org.openstreetmap.josm.tools.CheckParameterUtil; 022import org.openstreetmap.josm.tools.I18n; 023 024/** 025 * Validation error 026 * @since 3669 027 */ 028public class TestError implements Comparable<TestError> { 029 /** is this error on the ignore list */ 030 private boolean ignored; 031 /** Severity */ 032 private final Severity severity; 033 /** The error message */ 034 private final String message; 035 /** Deeper error description */ 036 private final String description; 037 private final String descriptionEn; 038 /** The affected primitives */ 039 private final Collection<? extends OsmPrimitive> primitives; 040 /** The primitives or way segments to be highlighted */ 041 private final Collection<?> highlighted; 042 /** The tester that raised this error */ 043 private final Test tester; 044 /** Internal code used by testers to classify errors */ 045 private final int code; 046 /** If this error is selected */ 047 private boolean selected; 048 /** Supplying a command to fix the error */ 049 private final Supplier<Command> fixingCommand; 050 051 /** 052 * A builder for a {@code TestError}. 053 * @since 11129 054 */ 055 public static final class Builder { 056 private final Test tester; 057 private final Severity severity; 058 private final int code; 059 private String message; 060 private String description; 061 private String descriptionEn; 062 private Collection<? extends OsmPrimitive> primitives; 063 private Collection<?> highlighted; 064 private Supplier<Command> fixingCommand; 065 066 Builder(Test tester, Severity severity, int code) { 067 this.tester = tester; 068 this.severity = severity; 069 this.code = code; 070 } 071 072 /** 073 * Sets the error message. 074 * 075 * @param message The error message 076 * @return {@code this} 077 */ 078 public Builder message(String message) { 079 this.message = message; 080 return this; 081 } 082 083 /** 084 * Sets the error message. 085 * 086 * @param message The the message of this error group 087 * @param description The translated description of this error 088 * @param descriptionEn The English description (for ignoring errors) 089 * @return {@code this} 090 */ 091 public Builder messageWithManuallyTranslatedDescription(String message, String description, String descriptionEn) { 092 this.message = message; 093 this.description = description; 094 this.descriptionEn = descriptionEn; 095 return this; 096 } 097 098 /** 099 * Sets the error message. 100 * 101 * @param message The the message of this error group 102 * @param marktrDescription The {@linkplain I18n#marktr prepared for i18n} description of this error 103 * @param args The description arguments to be applied in {@link I18n#tr(String, Object...)} 104 * @return {@code this} 105 */ 106 public Builder message(String message, String marktrDescription, Object... args) { 107 this.message = message; 108 this.description = I18n.tr(marktrDescription, args); 109 this.descriptionEn = new MessageFormat(marktrDescription, Locale.ENGLISH).format(args); 110 return this; 111 } 112 113 /** 114 * Sets the primitives affected by this error. 115 * 116 * @param primitives the primitives affected by this error 117 * @return {@code this} 118 */ 119 public Builder primitives(OsmPrimitive... primitives) { 120 return primitives(Arrays.asList(primitives)); 121 } 122 123 /** 124 * Sets the primitives affected by this error. 125 * 126 * @param primitives the primitives affected by this error 127 * @return {@code this} 128 */ 129 public Builder primitives(Collection<? extends OsmPrimitive> primitives) { 130 CheckParameterUtil.ensureThat(this.primitives == null, "primitives already set"); 131 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives"); 132 this.primitives = primitives; 133 if (this.highlighted == null) { 134 this.highlighted = primitives; 135 } 136 return this; 137 } 138 139 /** 140 * Sets the primitives to highlight when selecting this error. 141 * 142 * @param highlighted the primitives to highlight 143 * @return {@code this} 144 * @see ValidatorVisitor#visit(OsmPrimitive) 145 */ 146 public Builder highlight(OsmPrimitive... highlighted) { 147 return highlight(Arrays.asList(highlighted)); 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(Collection<? extends OsmPrimitive> highlighted) { 158 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted"); 159 this.highlighted = highlighted; 160 return this; 161 } 162 163 /** 164 * Sets the way segments to highlight when selecting this error. 165 * 166 * @param highlighted the way segments to highlight 167 * @return {@code this} 168 * @see ValidatorVisitor#visit(WaySegment) 169 */ 170 public Builder highlightWaySegments(Collection<WaySegment> highlighted) { 171 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted"); 172 this.highlighted = highlighted; 173 return this; 174 } 175 176 /** 177 * Sets the node pairs to highlight when selecting this error. 178 * 179 * @param highlighted the node pairs to highlight 180 * @return {@code this} 181 * @see ValidatorVisitor#visit(List) 182 */ 183 public Builder highlightNodePairs(Collection<List<Node>> highlighted) { 184 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted"); 185 this.highlighted = highlighted; 186 return this; 187 } 188 189 /** 190 * Sets a supplier to obtain a command to fix the error. 191 * 192 * @param fixingCommand the fix supplier 193 * @return {@code this} 194 */ 195 public Builder fix(Supplier<Command> fixingCommand) { 196 CheckParameterUtil.ensureThat(this.fixingCommand == null, "fixingCommand already set"); 197 this.fixingCommand = fixingCommand; 198 return this; 199 } 200 201 /** 202 * Returns a new test error with the specified values 203 * 204 * @return a new test error with the specified values 205 * @throws IllegalArgumentException when {@link #message} or {@link #primitives} is null/empty. 206 */ 207 public TestError build() { 208 CheckParameterUtil.ensureParameterNotNull(message, "message not set"); 209 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives not set"); 210 CheckParameterUtil.ensureThat(!primitives.isEmpty(), "primitives is empty"); 211 if (this.highlighted == null) { 212 this.highlighted = Collections.emptySet(); 213 } 214 return new TestError(this); 215 } 216 } 217 218 /** 219 * Starts building a new {@code TestError} 220 * @param tester The tester 221 * @param severity The severity of this error 222 * @param code The test error reference code 223 * @return a new test builder 224 * @since 11129 225 */ 226 public static Builder builder(Test tester, Severity severity, int code) { 227 return new Builder(tester, severity, code); 228 } 229 230 TestError(Builder builder) { 231 this.tester = builder.tester; 232 this.severity = builder.severity; 233 this.message = builder.message; 234 this.description = builder.description; 235 this.descriptionEn = builder.descriptionEn; 236 this.primitives = builder.primitives; 237 this.highlighted = builder.highlighted; 238 this.code = builder.code; 239 this.fixingCommand = builder.fixingCommand; 240 } 241 242 /** 243 * Gets the error message 244 * @return the error message 245 */ 246 public String getMessage() { 247 return message; 248 } 249 250 /** 251 * Gets the error message 252 * @return the error description 253 */ 254 public String getDescription() { 255 return description; 256 } 257 258 /** 259 * Gets the list of primitives affected by this error 260 * @return the list of primitives affected by this error 261 */ 262 public Collection<? extends OsmPrimitive> getPrimitives() { 263 return Collections.unmodifiableCollection(primitives); 264 } 265 266 /** 267 * Gets the severity of this error 268 * @return the severity of this error 269 */ 270 public Severity getSeverity() { 271 return severity; 272 } 273 274 /** 275 * Returns the ignore state for this error. 276 * @return the ignore state for this error 277 */ 278 public String getIgnoreState() { 279 Collection<String> strings = new TreeSet<>(); 280 StringBuilder ignorestring = new StringBuilder(getIgnoreSubGroup()); 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 for (String o : strings) { 296 ignorestring.append(':').append(o); 297 } 298 return ignorestring.toString(); 299 } 300 301 public String getIgnoreSubGroup() { 302 String ignorestring = getIgnoreGroup(); 303 if (descriptionEn != null) { 304 ignorestring += '_' + descriptionEn; 305 } 306 return ignorestring; 307 } 308 309 public String getIgnoreGroup() { 310 return Integer.toString(code); 311 } 312 313 public void setIgnored(boolean state) { 314 ignored = state; 315 } 316 317 public boolean isIgnored() { 318 return ignored; 319 } 320 321 /** 322 * Gets the tester that raised this error 323 * @return the tester that raised this error 324 */ 325 public Test getTester() { 326 return tester; 327 } 328 329 /** 330 * Gets the code 331 * @return the code 332 */ 333 public int getCode() { 334 return code; 335 } 336 337 /** 338 * Returns true if the error can be fixed automatically 339 * 340 * @return true if the error can be fixed 341 */ 342 public boolean isFixable() { 343 return fixingCommand != null || ((tester != null) && tester.isFixable(this)); 344 } 345 346 /** 347 * Fixes the error with the appropriate command 348 * 349 * @return The command to fix the error 350 */ 351 public Command getFix() { 352 // obtain fix from the error 353 final Command fix = fixingCommand != null ? fixingCommand.get() : null; 354 if (fix != null) { 355 return fix; 356 } 357 358 // obtain fix from the tester 359 if (tester == null || !tester.isFixable(this) || primitives.isEmpty()) 360 return null; 361 362 return tester.fixError(this); 363 } 364 365 /** 366 * Sets the selection flag of this error 367 * @param selected if this error is selected 368 */ 369 public void setSelected(boolean selected) { 370 this.selected = selected; 371 } 372 373 @SuppressWarnings("unchecked") 374 public void visitHighlighted(ValidatorVisitor v) { 375 for (Object o : highlighted) { 376 if (o instanceof OsmPrimitive) { 377 v.visit((OsmPrimitive) o); 378 } else if (o instanceof WaySegment) { 379 v.visit((WaySegment) o); 380 } else if (o instanceof List<?>) { 381 v.visit((List<Node>) o); 382 } 383 } 384 } 385 386 /** 387 * Returns the selection flag of this error 388 * @return true if this error is selected 389 * @since 5671 390 */ 391 public boolean isSelected() { 392 return selected; 393 } 394 395 /** 396 * Returns The primitives or way segments to be highlighted 397 * @return The primitives or way segments to be highlighted 398 * @since 5671 399 */ 400 public Collection<?> getHighlighted() { 401 return Collections.unmodifiableCollection(highlighted); 402 } 403 404 @Override 405 public int compareTo(TestError o) { 406 if (equals(o)) return 0; 407 408 MultipleNameVisitor v1 = new MultipleNameVisitor(); 409 MultipleNameVisitor v2 = new MultipleNameVisitor(); 410 411 v1.visit(getPrimitives()); 412 v2.visit(o.getPrimitives()); 413 return AlphanumComparator.getInstance().compare(v1.toString(), v2.toString()); 414 } 415 416 @Override 417 public String toString() { 418 return "TestError [tester=" + tester + ", code=" + code + ", message=" + message + ']'; 419 } 420}