001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation;
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.Collections;
007import java.util.List;
008import java.util.TreeSet;
009
010import org.openstreetmap.josm.Main;
011import org.openstreetmap.josm.command.Command;
012import org.openstreetmap.josm.data.osm.Node;
013import org.openstreetmap.josm.data.osm.OsmPrimitive;
014import org.openstreetmap.josm.data.osm.Relation;
015import org.openstreetmap.josm.data.osm.Way;
016import org.openstreetmap.josm.data.osm.WaySegment;
017import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
018import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
019import org.openstreetmap.josm.data.osm.event.DataSetListener;
020import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
021import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
022import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
023import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
024import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
025import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
026import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
027import org.openstreetmap.josm.tools.AlphanumComparator;
028
029/**
030 * Validation error
031 * @since 3669
032 */
033public class TestError implements Comparable<TestError>, DataSetListener {
034    /** is this error on the ignore list */
035    private boolean ignored;
036    /** Severity */
037    private Severity severity;
038    /** The error message */
039    private String message;
040    /** Deeper error description */
041    private String description;
042    private String descriptionEn;
043    /** The affected primitives */
044    private Collection<? extends OsmPrimitive> primitives;
045    /** The primitives or way segments to be highlighted */
046    private Collection<?> highlighted;
047    /** The tester that raised this error */
048    private Test tester;
049    /** Internal code used by testers to classify errors */
050    private int code;
051    /** If this error is selected */
052    private boolean selected;
053
054    /**
055     * Constructs a new {@code TestError}.
056     * @param tester The tester
057     * @param severity The severity of this error
058     * @param message The error message
059     * @param description The translated description
060     * @param descriptionEn The English description
061     * @param primitives The affected primitives
062     * @param code The test error reference code
063     * @param highlighted OSM primitives to highlight
064     */
065    public TestError(Test tester, Severity severity, String message, String description, String descriptionEn,
066            int code, Collection<? extends OsmPrimitive> primitives, Collection<?> highlighted) {
067        this.tester = tester;
068        this.severity = severity;
069        this.message = message;
070        this.description = description;
071        this.descriptionEn = descriptionEn;
072        this.primitives = primitives;
073        this.highlighted = highlighted;
074        this.code = code;
075    }
076
077    /**
078     * Constructs a new {@code TestError} without description.
079     * @param tester The tester
080     * @param severity The severity of this error
081     * @param message The error message
082     * @param primitives The affected primitives
083     * @param code The test error reference code
084     * @param highlighted OSM primitives to highlight
085     */
086    public TestError(Test tester, Severity severity, String message, int code, Collection<? extends OsmPrimitive> primitives,
087            Collection<?> highlighted) {
088        this(tester, severity, message, null, null, code, primitives, highlighted);
089    }
090
091    /**
092     * Constructs a new {@code TestError}.
093     * @param tester The tester
094     * @param severity The severity of this error
095     * @param message The error message
096     * @param description The translated description
097     * @param descriptionEn The English description
098     * @param primitives The affected primitives
099     * @param code The test error reference code
100     */
101    public TestError(Test tester, Severity severity, String message, String description, String descriptionEn,
102            int code, Collection<? extends OsmPrimitive> primitives) {
103        this(tester, severity, message, description, descriptionEn, code, primitives, primitives);
104    }
105
106    /**
107     * Constructs a new {@code TestError} without description.
108     * @param tester The tester
109     * @param severity The severity of this error
110     * @param message The error message
111     * @param primitives The affected primitives
112     * @param code The test error reference code
113     */
114    public TestError(Test tester, Severity severity, String message, int code, Collection<? extends OsmPrimitive> primitives) {
115        this(tester, severity, message, null, null, code, primitives, primitives);
116    }
117
118    /**
119     * Constructs a new {@code TestError} without description, for a single primitive.
120     * @param tester The tester
121     * @param severity The severity of this error
122     * @param message The error message
123     * @param primitive The affected primitive
124     * @param code The test error reference code
125     */
126    public TestError(Test tester, Severity severity, String message, int code, OsmPrimitive primitive) {
127        this(tester, severity, message, null, null, code, Collections.singletonList(primitive), Collections
128                .singletonList(primitive));
129    }
130
131    /**
132     * Constructs a new {@code TestError} for a single primitive.
133     * @param tester The tester
134     * @param severity The severity of this error
135     * @param message The error message
136     * @param description The translated description
137     * @param descriptionEn The English description
138     * @param primitive The affected primitive
139     * @param code The test error reference code
140     */
141    public TestError(Test tester, Severity severity, String message, String description, String descriptionEn,
142            int code, OsmPrimitive primitive) {
143        this(tester, severity, message, description, descriptionEn, code, Collections.singletonList(primitive));
144    }
145
146    /**
147     * Gets the error message
148     * @return the error message
149     */
150    public String getMessage() {
151        return message;
152    }
153
154    /**
155     * Gets the error message
156     * @return the error description
157     */
158    public String getDescription() {
159        return description;
160    }
161
162    /**
163     * Sets the error message
164     * @param message The error message
165     */
166    public void setMessage(String message) {
167        this.message = message;
168    }
169
170    /**
171     * Gets the list of primitives affected by this error
172     * @return the list of primitives affected by this error
173     */
174    public Collection<? extends OsmPrimitive> getPrimitives() {
175        return primitives;
176    }
177
178    /**
179     * Gets the list of primitives affected by this error and are selectable
180     * @return the list of selectable primitives affected by this error
181     */
182    public Collection<? extends OsmPrimitive> getSelectablePrimitives() {
183        List<OsmPrimitive> selectablePrimitives = new ArrayList<>(primitives.size());
184        for (OsmPrimitive o : primitives) {
185            if (o.isSelectable()) {
186                selectablePrimitives.add(o);
187            }
188        }
189        return selectablePrimitives;
190    }
191
192    /**
193     * Sets the list of primitives affected by this error
194     * @param primitives the list of primitives affected by this error
195     */
196    public void setPrimitives(List<? extends OsmPrimitive> primitives) {
197        this.primitives = primitives;
198    }
199
200    /**
201     * Gets the severity of this error
202     * @return the severity of this error
203     */
204    public Severity getSeverity() {
205        return severity;
206    }
207
208    /**
209     * Sets the severity of this error
210     * @param severity the severity of this error
211     */
212    public void setSeverity(Severity severity) {
213        this.severity = severity;
214    }
215
216    /**
217     * Returns the ignore state for this error.
218     * @return the ignore state for this error
219     */
220    public String getIgnoreState() {
221        Collection<String> strings = new TreeSet<>();
222        StringBuilder ignorestring = new StringBuilder(getIgnoreSubGroup());
223        for (OsmPrimitive o : primitives) {
224            // ignore data not yet uploaded
225            if (o.isNew())
226                return null;
227            String type = "u";
228            if (o instanceof Way) {
229                type = "w";
230            } else if (o instanceof Relation) {
231                type = "r";
232            } else if (o instanceof Node) {
233                type = "n";
234            }
235            strings.add(type + '_' + o.getId());
236        }
237        for (String o : strings) {
238            ignorestring.append(':').append(o);
239        }
240        return ignorestring.toString();
241    }
242
243    public String getIgnoreSubGroup() {
244        String ignorestring = getIgnoreGroup();
245        if (descriptionEn != null) {
246            ignorestring += '_' + descriptionEn;
247        }
248        return ignorestring;
249    }
250
251    public String getIgnoreGroup() {
252        return Integer.toString(code);
253    }
254
255    public void setIgnored(boolean state) {
256        ignored = state;
257    }
258
259    public boolean isIgnored() {
260        return ignored;
261    }
262
263    /**
264     * Gets the tester that raised this error
265     * @return the tester that raised this error
266     */
267    public Test getTester() {
268        return tester;
269    }
270
271    public void setTester(Test tester) {
272        this.tester = tester;
273    }
274
275    /**
276     * Gets the code
277     * @return the code
278     */
279    public int getCode() {
280        return code;
281    }
282
283    /**
284     * Returns true if the error can be fixed automatically
285     *
286     * @return true if the error can be fixed
287     */
288    public boolean isFixable() {
289        return tester != null && tester.isFixable(this);
290    }
291
292    /**
293     * Fixes the error with the appropriate command
294     *
295     * @return The command to fix the error
296     */
297    public Command getFix() {
298        if (tester == null || !tester.isFixable(this) || primitives.isEmpty())
299            return null;
300
301        return tester.fixError(this);
302    }
303
304    /**
305     * Sets the selection flag of this error
306     * @param selected if this error is selected
307     */
308    public void setSelected(boolean selected) {
309        this.selected = selected;
310    }
311
312    @SuppressWarnings("unchecked")
313    public void visitHighlighted(ValidatorVisitor v) {
314        for (Object o : highlighted) {
315            if (o instanceof OsmPrimitive) {
316                v.visit((OsmPrimitive) o);
317            } else if (o instanceof WaySegment) {
318                v.visit((WaySegment) o);
319            } else if (o instanceof List<?>) {
320                v.visit((List<Node>) o);
321            }
322        }
323    }
324
325    /**
326     * Returns the selection flag of this error
327     * @return true if this error is selected
328     * @since 5671
329     */
330    public boolean isSelected() {
331        return selected;
332    }
333
334    /**
335     * Returns The primitives or way segments to be highlighted
336     * @return The primitives or way segments to be highlighted
337     * @since 5671
338     */
339    public Collection<?> getHighlighted() {
340        return highlighted;
341    }
342
343    @Override
344    public int compareTo(TestError o) {
345        if (equals(o)) return 0;
346
347        MultipleNameVisitor v1 = new MultipleNameVisitor();
348        MultipleNameVisitor v2 = new MultipleNameVisitor();
349
350        v1.visit(getPrimitives());
351        v2.visit(o.getPrimitives());
352        return AlphanumComparator.getInstance().compare(v1.toString(), v2.toString());
353    }
354
355    @Override public void primitivesRemoved(PrimitivesRemovedEvent event) {
356        // Remove purged primitives (fix #8639)
357        try {
358            primitives.removeAll(event.getPrimitives());
359        } catch (UnsupportedOperationException e) {
360            if (event.getPrimitives().containsAll(primitives)) {
361                primitives = Collections.emptyList();
362            } else {
363                Main.warn(e, "Unable to remove primitives from "+this+'.');
364            }
365        }
366    }
367
368    @Override public void primitivesAdded(PrimitivesAddedEvent event) {
369        // Do nothing
370    }
371
372    @Override public void tagsChanged(TagsChangedEvent event) {
373        // Do nothing
374    }
375
376    @Override public void nodeMoved(NodeMovedEvent event) {
377        // Do nothing
378    }
379
380    @Override public void wayNodesChanged(WayNodesChangedEvent event) {
381        // Do nothing
382    }
383
384    @Override public void relationMembersChanged(RelationMembersChangedEvent event) {
385        // Do nothing
386    }
387
388    @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {
389        // Do nothing
390    }
391
392    @Override public void dataChanged(DataChangedEvent event) {
393        // Do nothing
394    }
395
396    @Override
397    public String toString() {
398        return "TestError [tester=" + tester + ", code=" + code + ", message=" + message + ']';
399    }
400}