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}