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