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