001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation;
003
004import java.text.MessageFormat;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.List;
010import java.util.Locale;
011import java.util.TreeSet;
012import java.util.function.Supplier;
013
014import org.openstreetmap.josm.Main;
015import org.openstreetmap.josm.command.Command;
016import org.openstreetmap.josm.data.osm.Node;
017import org.openstreetmap.josm.data.osm.OsmPrimitive;
018import org.openstreetmap.josm.data.osm.Relation;
019import org.openstreetmap.josm.data.osm.Way;
020import org.openstreetmap.josm.data.osm.WaySegment;
021import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
022import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
023import org.openstreetmap.josm.data.osm.event.DataSetListener;
024import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
025import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
026import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
027import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
028import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
029import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
030import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
031import org.openstreetmap.josm.tools.AlphanumComparator;
032import org.openstreetmap.josm.tools.CheckParameterUtil;
033import org.openstreetmap.josm.tools.I18n;
034
035/**
036 * Validation error
037 * @since 3669
038 */
039public class TestError implements Comparable<TestError>, DataSetListener {
040    /** is this error on the ignore list */
041    private boolean ignored;
042    /** Severity */
043    private Severity severity;
044    /** The error message */
045    private String message;
046    /** Deeper error description */
047    private final String description;
048    private final String descriptionEn;
049    /** The affected primitives */
050    private Collection<? extends OsmPrimitive> primitives;
051    /** The primitives or way segments to be highlighted */
052    private final Collection<?> highlighted;
053    /** The tester that raised this error */
054    private Test tester;
055    /** Internal code used by testers to classify errors */
056    private final int code;
057    /** If this error is selected */
058    private boolean selected;
059    /** Supplying a command to fix the error */
060    private final Supplier<Command> fixingCommand;
061
062    /**
063     * A builder for a {@code TestError}.
064     * @since 11129
065     */
066    public static final class Builder {
067        private final Test tester;
068        private final Severity severity;
069        private final int code;
070        private String message;
071        private String description;
072        private String descriptionEn;
073        private Collection<? extends OsmPrimitive> primitives;
074        private Collection<?> highlighted;
075        private Supplier<Command> fixingCommand;
076
077        Builder(Test tester, Severity severity, int code) {
078            this.tester = tester;
079            this.severity = severity;
080            this.code = code;
081        }
082
083        /**
084         * Sets the error message.
085         *
086         * @param message The error message
087         * @return {@code this}
088         */
089        public Builder message(String message) {
090            this.message = message;
091            return this;
092        }
093
094        /**
095         * Sets the error message.
096         *
097         * @param message       The the message of this error group
098         * @param description   The translated description of this error
099         * @param descriptionEn The English description (for ignoring errors)
100         * @return {@code this}
101         */
102        public Builder messageWithManuallyTranslatedDescription(String message, String description, String descriptionEn) {
103            this.message = message;
104            this.description = description;
105            this.descriptionEn = descriptionEn;
106            return this;
107        }
108
109        /**
110         * Sets the error message.
111         *
112         * @param message The the message of this error group
113         * @param marktrDescription The {@linkplain I18n#marktr prepared for i18n} description of this error
114         * @param args The description arguments to be applied in {@link I18n#tr(String, Object...)}
115         * @return {@code this}
116         */
117        public Builder message(String message, String marktrDescription, Object... args) {
118            this.message = message;
119            this.description = I18n.tr(marktrDescription, args);
120            this.descriptionEn = new MessageFormat(marktrDescription, Locale.ENGLISH).format(args);
121            return this;
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(OsmPrimitive... primitives) {
131            return primitives(Arrays.asList(primitives));
132        }
133
134        /**
135         * Sets the primitives affected by this error.
136         *
137         * @param primitives the primitives affected by this error
138         * @return {@code this}
139         */
140        public Builder primitives(Collection<? extends OsmPrimitive> primitives) {
141            CheckParameterUtil.ensureThat(this.primitives == null, "primitives already set");
142            CheckParameterUtil.ensureParameterNotNull(primitives, "primitives");
143            this.primitives = primitives;
144            if (this.highlighted == null) {
145                this.highlighted = primitives;
146            }
147            return this;
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(OsmPrimitive... highlighted) {
158            return highlight(Arrays.asList(highlighted));
159        }
160
161        /**
162         * Sets the primitives to highlight when selecting this error.
163         *
164         * @param highlighted the primitives to highlight
165         * @return {@code this}
166         * @see ValidatorVisitor#visit(OsmPrimitive)
167         */
168        public Builder highlight(Collection<? extends OsmPrimitive> highlighted) {
169            CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
170            this.highlighted = highlighted;
171            return this;
172        }
173
174        /**
175         * Sets the way segments to highlight when selecting this error.
176         *
177         * @param highlighted the way segments to highlight
178         * @return {@code this}
179         * @see ValidatorVisitor#visit(WaySegment)
180         */
181        public Builder highlightWaySegments(Collection<WaySegment> highlighted) {
182            CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
183            this.highlighted = highlighted;
184            return this;
185        }
186
187        /**
188         * Sets the node pairs to highlight when selecting this error.
189         *
190         * @param highlighted the node pairs to highlight
191         * @return {@code this}
192         * @see ValidatorVisitor#visit(List)
193         */
194        public Builder highlightNodePairs(Collection<List<Node>> highlighted) {
195            CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
196            this.highlighted = highlighted;
197            return this;
198        }
199
200        /**
201         * Sets a supplier to obtain a command to fix the error.
202         *
203         * @param fixingCommand the fix supplier
204         * @return {@code this}
205         */
206        public Builder fix(Supplier<Command> fixingCommand) {
207            CheckParameterUtil.ensureThat(this.fixingCommand == null, "fixingCommand already set");
208            this.fixingCommand = fixingCommand;
209            return this;
210        }
211
212        /**
213         * Returns a new test error with the specified values
214         *
215         * @return a new test error with the specified values
216         * @throws IllegalArgumentException when {@link #message} or {@link #primitives} is null/empty.
217         */
218        public TestError build() {
219            CheckParameterUtil.ensureParameterNotNull(message, "message not set");
220            CheckParameterUtil.ensureParameterNotNull(primitives, "primitives not set");
221            CheckParameterUtil.ensureThat(!primitives.isEmpty(), "primitives is empty");
222            if (this.highlighted == null) {
223                this.highlighted = Collections.emptySet();
224            }
225            return new TestError(this);
226        }
227    }
228
229    /**
230     * Starts building a new {@code TestError}
231     * @param tester The tester
232     * @param severity The severity of this error
233     * @param code The test error reference code
234     * @return a new test builder
235     * @since 11129
236     */
237    public static Builder builder(Test tester, Severity severity, int code) {
238        return new Builder(tester, severity, code);
239    }
240
241    TestError(Builder builder) {
242        this.tester = builder.tester;
243        this.severity = builder.severity;
244        this.message = builder.message;
245        this.description = builder.description;
246        this.descriptionEn = builder.descriptionEn;
247        this.primitives = builder.primitives;
248        this.highlighted = builder.highlighted;
249        this.code = builder.code;
250        this.fixingCommand = builder.fixingCommand;
251    }
252
253    /**
254     * Constructs a new {@code TestError}.
255     * @param tester The tester
256     * @param severity The severity of this error
257     * @param message The error message
258     * @param description The translated description
259     * @param descriptionEn The English description
260     * @param code The test error reference code
261     * @param primitives The affected primitives
262     * @param highlighted OSM primitives to highlight
263     * @deprecated Use {@link #builder} instead. Will be removed in 2016-12.
264     */
265    @Deprecated
266    public TestError(Test tester, Severity severity, String message, String description, String descriptionEn,
267            int code, Collection<? extends OsmPrimitive> primitives, Collection<?> highlighted) {
268        this.tester = tester;
269        this.severity = severity;
270        this.message = message;
271        this.description = description;
272        this.descriptionEn = descriptionEn;
273        this.primitives = primitives;
274        this.highlighted = highlighted;
275        this.code = code;
276        this.fixingCommand = null;
277    }
278
279    /**
280     * Constructs a new {@code TestError} without description.
281     * @param tester The tester
282     * @param severity The severity of this error
283     * @param message The error message
284     * @param code The test error reference code
285     * @param primitives The affected primitives
286     * @param highlighted OSM primitives to highlight
287     * @deprecated Use {@link #builder} instead. Will be removed in 2016-12.
288     */
289    @Deprecated
290    public TestError(Test tester, Severity severity, String message, int code, Collection<? extends OsmPrimitive> primitives,
291            Collection<?> highlighted) {
292        this(tester, severity, message, null, null, code, primitives, highlighted);
293    }
294
295    /**
296     * Constructs a new {@code TestError}.
297     * @param tester The tester
298     * @param severity The severity of this error
299     * @param message The error message
300     * @param description The translated description
301     * @param descriptionEn The English description
302     * @param code The test error reference code
303     * @param primitives The affected primitives
304     * @deprecated Use {@link #builder} instead. Will be removed in 2016-12.
305     */
306    @Deprecated
307    public TestError(Test tester, Severity severity, String message, String description, String descriptionEn,
308            int code, Collection<? extends OsmPrimitive> primitives) {
309        this(tester, severity, message, description, descriptionEn, code, primitives, primitives);
310    }
311
312    /**
313     * Constructs a new {@code TestError} without description.
314     * @param tester The tester
315     * @param severity The severity of this error
316     * @param message The error message
317     * @param code The test error reference code
318     * @param primitives The affected primitives
319     * @deprecated Use {@link #builder} instead. Will be removed in 2016-12.
320     */
321    @Deprecated
322    public TestError(Test tester, Severity severity, String message, int code, Collection<? extends OsmPrimitive> primitives) {
323        this(tester, severity, message, null, null, code, primitives, primitives);
324    }
325
326    /**
327     * Constructs a new {@code TestError} without description, for a single primitive.
328     * @param tester The tester
329     * @param severity The severity of this error
330     * @param message The error message
331     * @param code The test error reference code
332     * @param primitive The affected primitive
333     * @deprecated Use {@link #builder} instead. Will be removed in 2016-12.
334     */
335    @Deprecated
336    public TestError(Test tester, Severity severity, String message, int code, OsmPrimitive primitive) {
337        this(tester, severity, message, null, null, code, Collections.singletonList(primitive), Collections
338                .singletonList(primitive));
339    }
340
341    /**
342     * Constructs a new {@code TestError} for a single primitive.
343     * @param tester The tester
344     * @param severity The severity of this error
345     * @param message The error message
346     * @param description The translated description
347     * @param descriptionEn The English description
348     * @param code The test error reference code
349     * @param primitive The affected primitive
350     * @deprecated Use {@link #builder} instead. Will be removed in 2016-12.
351     */
352    @Deprecated
353    public TestError(Test tester, Severity severity, String message, String description, String descriptionEn,
354            int code, OsmPrimitive primitive) {
355        this(tester, severity, message, description, descriptionEn, code, Collections.singletonList(primitive));
356    }
357
358    /**
359     * Gets the error message
360     * @return the error message
361     */
362    public String getMessage() {
363        return message;
364    }
365
366    /**
367     * Gets the error message
368     * @return the error description
369     */
370    public String getDescription() {
371        return description;
372    }
373
374    /**
375     * Sets the error message
376     * @param message The error message
377     * @deprecated Use {@link #builder} instead. Will be removed in 2016-12.
378     */
379    @Deprecated
380    public void setMessage(String message) {
381        this.message = message;
382    }
383
384    /**
385     * Gets the list of primitives affected by this error
386     * @return the list of primitives affected by this error
387     */
388    public Collection<? extends OsmPrimitive> getPrimitives() {
389        return primitives;
390    }
391
392    /**
393     * Gets the list of primitives affected by this error and are selectable
394     * @return the list of selectable primitives affected by this error
395     */
396    public Collection<? extends OsmPrimitive> getSelectablePrimitives() {
397        List<OsmPrimitive> selectablePrimitives = new ArrayList<>(primitives.size());
398        for (OsmPrimitive o : primitives) {
399            if (o.isSelectable()) {
400                selectablePrimitives.add(o);
401            }
402        }
403        return selectablePrimitives;
404    }
405
406    /**
407     * Sets the list of primitives affected by this error
408     * @param primitives the list of primitives affected by this error*
409     * @deprecated Use {@link #builder} instead. Will be removed in 2016-12.
410     */
411    @Deprecated
412    public void setPrimitives(List<? extends OsmPrimitive> primitives) {
413        this.primitives = primitives;
414    }
415
416    /**
417     * Gets the severity of this error
418     * @return the severity of this error
419     */
420    public Severity getSeverity() {
421        return severity;
422    }
423
424    /**
425     * Sets the severity of this error
426     * @param severity the severity of this error
427     * @deprecated Use {@link #builder} instead. Will be removed in 2016-12.
428     */
429    @Deprecated
430    public void setSeverity(Severity severity) {
431        this.severity = severity;
432    }
433
434    /**
435     * Returns the ignore state for this error.
436     * @return the ignore state for this error
437     */
438    public String getIgnoreState() {
439        Collection<String> strings = new TreeSet<>();
440        StringBuilder ignorestring = new StringBuilder(getIgnoreSubGroup());
441        for (OsmPrimitive o : primitives) {
442            // ignore data not yet uploaded
443            if (o.isNew())
444                return null;
445            String type = "u";
446            if (o instanceof Way) {
447                type = "w";
448            } else if (o instanceof Relation) {
449                type = "r";
450            } else if (o instanceof Node) {
451                type = "n";
452            }
453            strings.add(type + '_' + o.getId());
454        }
455        for (String o : strings) {
456            ignorestring.append(':').append(o);
457        }
458        return ignorestring.toString();
459    }
460
461    public String getIgnoreSubGroup() {
462        String ignorestring = getIgnoreGroup();
463        if (descriptionEn != null) {
464            ignorestring += '_' + descriptionEn;
465        }
466        return ignorestring;
467    }
468
469    public String getIgnoreGroup() {
470        return Integer.toString(code);
471    }
472
473    public void setIgnored(boolean state) {
474        ignored = state;
475    }
476
477    public boolean isIgnored() {
478        return ignored;
479    }
480
481    /**
482     * Gets the tester that raised this error
483     * @return the tester that raised this error
484     */
485    public Test getTester() {
486        return tester;
487    }
488
489    /**
490     * Set the tester that raised the error.
491     * @param tester te tester
492     * @deprecated Use {@link #builder} instead. Will be removed in 2016-12.
493     */
494    @Deprecated
495    public void setTester(Test tester) {
496        this.tester = tester;
497    }
498
499    /**
500     * Gets the code
501     * @return the code
502     */
503    public int getCode() {
504        return code;
505    }
506
507    /**
508     * Returns true if the error can be fixed automatically
509     *
510     * @return true if the error can be fixed
511     */
512    public boolean isFixable() {
513        return fixingCommand != null || ((tester != null) && tester.isFixable(this));
514    }
515
516    /**
517     * Fixes the error with the appropriate command
518     *
519     * @return The command to fix the error
520     */
521    public Command getFix() {
522        // obtain fix from the error
523        final Command fix = fixingCommand != null ? fixingCommand.get() : null;
524        if (fix != null) {
525            return fix;
526        }
527
528        // obtain fix from the tester
529        if (tester == null || !tester.isFixable(this) || primitives.isEmpty())
530            return null;
531
532        return tester.fixError(this);
533    }
534
535    /**
536     * Sets the selection flag of this error
537     * @param selected if this error is selected
538     */
539    public void setSelected(boolean selected) {
540        this.selected = selected;
541    }
542
543    @SuppressWarnings("unchecked")
544    public void visitHighlighted(ValidatorVisitor v) {
545        for (Object o : highlighted) {
546            if (o instanceof OsmPrimitive) {
547                v.visit((OsmPrimitive) o);
548            } else if (o instanceof WaySegment) {
549                v.visit((WaySegment) o);
550            } else if (o instanceof List<?>) {
551                v.visit((List<Node>) o);
552            }
553        }
554    }
555
556    /**
557     * Returns the selection flag of this error
558     * @return true if this error is selected
559     * @since 5671
560     */
561    public boolean isSelected() {
562        return selected;
563    }
564
565    /**
566     * Returns The primitives or way segments to be highlighted
567     * @return The primitives or way segments to be highlighted
568     * @since 5671
569     */
570    public Collection<?> getHighlighted() {
571        return highlighted;
572    }
573
574    @Override
575    public int compareTo(TestError o) {
576        if (equals(o)) return 0;
577
578        MultipleNameVisitor v1 = new MultipleNameVisitor();
579        MultipleNameVisitor v2 = new MultipleNameVisitor();
580
581        v1.visit(getPrimitives());
582        v2.visit(o.getPrimitives());
583        return AlphanumComparator.getInstance().compare(v1.toString(), v2.toString());
584    }
585
586    @Override public void primitivesRemoved(PrimitivesRemovedEvent event) {
587        // Remove purged primitives (fix #8639)
588        try {
589            primitives.removeAll(event.getPrimitives());
590        } catch (UnsupportedOperationException e) {
591            if (event.getPrimitives().containsAll(primitives)) {
592                primitives = Collections.emptyList();
593            } else {
594                Main.warn(e, "Unable to remove primitives from "+this+'.');
595            }
596        }
597    }
598
599    @Override public void primitivesAdded(PrimitivesAddedEvent event) {
600        // Do nothing
601    }
602
603    @Override public void tagsChanged(TagsChangedEvent event) {
604        // Do nothing
605    }
606
607    @Override public void nodeMoved(NodeMovedEvent event) {
608        // Do nothing
609    }
610
611    @Override public void wayNodesChanged(WayNodesChangedEvent event) {
612        // Do nothing
613    }
614
615    @Override public void relationMembersChanged(RelationMembersChangedEvent event) {
616        // Do nothing
617    }
618
619    @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {
620        // Do nothing
621    }
622
623    @Override public void dataChanged(DataChangedEvent event) {
624        // Do nothing
625    }
626
627    @Override
628    public String toString() {
629        return "TestError [tester=" + tester + ", code=" + code + ", message=" + message + ']';
630    }
631}