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}