001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.io.PrintWriter;
005import java.io.StringWriter;
006import java.io.Writer;
007
008import org.openstreetmap.josm.tools.JosmRuntimeException;
009import org.openstreetmap.josm.tools.Logging;
010import org.openstreetmap.josm.tools.Stopwatch;
011
012/**
013 * This class can be used to run consistency tests on dataset. Any errors found will be written to provided PrintWriter.
014 * <br>
015 * Texts here should not be translated because they're not intended for users but for josm developers.
016 * @since 2500
017 */
018public class DatasetConsistencyTest {
019
020    private static final int MAX_ERRORS = 100;
021    private final DataSet dataSet;
022    private final PrintWriter writer;
023    private int errorCount;
024
025    /**
026     * Constructs a new {@code DatasetConsistencyTest}.
027     * @param dataSet The dataset to test
028     * @param writer The writer used to write results
029     */
030    public DatasetConsistencyTest(DataSet dataSet, Writer writer) {
031        this.dataSet = dataSet;
032        this.writer = new PrintWriter(writer);
033    }
034
035    private void printError(String type, String message, Object... args) {
036        errorCount++;
037        if (errorCount <= MAX_ERRORS) {
038            writer.println('[' + type + "] " + String.format(message, args));
039        }
040    }
041
042    /**
043     * Checks that parent primitive is referred from its child members
044     */
045    public void checkReferrers() {
046        final Stopwatch stopwatch = Stopwatch.createStarted();
047        // It's also error when referred primitive's dataset is null but it's already covered by referredPrimitiveNotInDataset check
048        for (Way way : dataSet.getWays()) {
049            if (!way.isDeleted()) {
050                for (Node n : way.getNodes()) {
051                    if (n.getDataSet() != null && !n.getReferrers().contains(way)) {
052                        printError("WAY NOT IN REFERRERS", "%s is part of %s but is not in referrers", n, way);
053                    }
054                }
055            }
056        }
057
058        for (Relation relation : dataSet.getRelations()) {
059            if (!relation.isDeleted()) {
060                for (RelationMember m : relation.getMembers()) {
061                    if (m.getMember().getDataSet() != null && !m.getMember().getReferrers().contains(relation)) {
062                        printError("RELATION NOT IN REFERRERS", "%s is part of %s but is not in referrers", m.getMember(), relation);
063                    }
064                }
065            }
066        }
067        printElapsedTime(stopwatch);
068    }
069
070    /**
071     * Checks for complete ways with incomplete nodes.
072     */
073    public void checkCompleteWaysWithIncompleteNodes() {
074        final Stopwatch stopwatch = Stopwatch.createStarted();
075        for (Way way : dataSet.getWays()) {
076            if (way.isUsable()) {
077                for (Node node : way.getNodes()) {
078                    if (node.isIncomplete()) {
079                        printError("USABLE HAS INCOMPLETE", "%s is usable but contains incomplete node '%s'", way, node);
080                    }
081                }
082            }
083        }
084        printElapsedTime(stopwatch);
085    }
086
087    /**
088     * Checks for complete nodes without coordinates.
089     */
090    public void checkCompleteNodesWithoutCoordinates() {
091        final Stopwatch stopwatch = Stopwatch.createStarted();
092        for (Node node : dataSet.getNodes()) {
093            if (!node.isIncomplete() && node.isVisible() && !node.isLatLonKnown()) {
094                printError("COMPLETE WITHOUT COORDINATES", "%s is not incomplete but has null coordinates", node);
095            }
096        }
097        printElapsedTime(stopwatch);
098    }
099
100    /**
101     * Checks that nodes can be retrieved through their coordinates.
102     */
103    public void searchNodes() {
104        final Stopwatch stopwatch = Stopwatch.createStarted();
105        dataSet.getReadLock().lock();
106        try {
107            for (Node n : dataSet.getNodes()) {
108                // Call isDrawable() as an efficient replacement to previous checks (!deleted, !incomplete, getCoor() != null)
109                if (n.isDrawable() && !dataSet.containsNode(n)) {
110                    printError("SEARCH NODES", "%s not found using Dataset.containsNode()", n);
111                }
112            }
113        } finally {
114            dataSet.getReadLock().unlock();
115        }
116        printElapsedTime(stopwatch);
117    }
118
119    /**
120     * Checks that ways can be retrieved through their bounding box.
121     */
122    public void searchWays() {
123        final Stopwatch stopwatch = Stopwatch.createStarted();
124        dataSet.getReadLock().lock();
125        try {
126            for (Way w : dataSet.getWays()) {
127                if (!w.isIncomplete() && !w.isDeleted() && w.getNodesCount() >= 2 && !dataSet.containsWay(w)) {
128                    printError("SEARCH WAYS", "%s not found using Dataset.containsWay()", w);
129                }
130            }
131        } finally {
132            dataSet.getReadLock().unlock();
133        }
134        printElapsedTime(stopwatch);
135    }
136
137    private void checkReferredPrimitive(OsmPrimitive primitive, OsmPrimitive parent) {
138        if (primitive.getDataSet() == null) {
139            printError("NO DATASET", "%s is referenced by %s but not found in dataset", primitive, parent);
140        } else if (dataSet.getPrimitiveById(primitive) == null) {
141            printError("REFERENCED BUT NOT IN DATA", "%s is referenced by %s but not found in dataset", primitive, parent);
142        } else if (dataSet.getPrimitiveById(primitive) != primitive) {
143            printError("DIFFERENT INSTANCE", "%s is different instance that referred by %s", primitive, parent);
144        }
145
146        if (primitive.isDeleted()) {
147            printError("DELETED REFERENCED", "%s refers to deleted primitive %s", parent, primitive);
148        }
149    }
150
151    /**
152     * Checks that referred primitives are present in dataset.
153     */
154    public void referredPrimitiveNotInDataset() {
155        final Stopwatch stopwatch = Stopwatch.createStarted();
156        for (Way way : dataSet.getWays()) {
157            for (Node node : way.getNodes()) {
158                checkReferredPrimitive(node, way);
159            }
160        }
161
162        for (Relation relation : dataSet.getRelations()) {
163            for (RelationMember member : relation.getMembers()) {
164                checkReferredPrimitive(member.getMember(), relation);
165            }
166        }
167        printElapsedTime(stopwatch);
168    }
169
170    /**
171     * Checks for zero and one-node ways.
172     */
173    public void checkZeroNodesWays() {
174        final Stopwatch stopwatch = Stopwatch.createStarted();
175        for (Way way : dataSet.getWays()) {
176            if (way.isUsable() && way.isEmpty()) {
177                printError("WARN - ZERO NODES", "Way %s has zero nodes", way);
178            } else if (way.isUsable() && way.getNodesCount() == 1) {
179                printError("WARN - NO NODES", "Way %s has only one node", way);
180            }
181        }
182        printElapsedTime(stopwatch);
183    }
184
185    private void printElapsedTime(Stopwatch stopwatch) {
186        if (Logging.isDebugEnabled()) {
187            StackTraceElement item = Thread.currentThread().getStackTrace()[2];
188            String operation = getClass().getSimpleName() + '.' + item.getMethodName();
189            Logging.debug(stopwatch.toString(operation));
190        }
191    }
192
193    /**
194     * Runs test.
195     */
196    public void runTest() {
197        try {
198            final Stopwatch stopwatch = Stopwatch.createStarted();
199            referredPrimitiveNotInDataset();
200            checkReferrers();
201            checkCompleteWaysWithIncompleteNodes();
202            checkCompleteNodesWithoutCoordinates();
203            searchNodes();
204            searchWays();
205            checkZeroNodesWays();
206            printElapsedTime(stopwatch);
207            if (errorCount > MAX_ERRORS) {
208                writer.println((errorCount - MAX_ERRORS) + " more...");
209            }
210
211        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
212            writer.println("Exception during dataset integrity test:");
213            e.printStackTrace(writer);
214            Logging.warn(e);
215        }
216    }
217
218    /**
219     * Runs test on the given dataset.
220     * @param dataSet the dataset to test
221     * @return the errors as string
222     */
223    public static String runTests(DataSet dataSet) {
224        StringWriter writer = new StringWriter();
225        new DatasetConsistencyTest(dataSet, writer).runTest();
226        return writer.toString();
227    }
228}