001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import java.io.BufferedWriter;
005import java.io.IOException;
006import java.io.OutputStream;
007import java.io.OutputStreamWriter;
008import java.io.PrintWriter;
009import java.nio.charset.StandardCharsets;
010import java.time.Instant;
011import java.util.ArrayList;
012import java.util.Collection;
013import java.util.Comparator;
014import java.util.HashMap;
015import java.util.LinkedHashSet;
016import java.util.List;
017import java.util.Map;
018import java.util.Map.Entry;
019import java.util.Set;
020import java.util.stream.Collectors;
021
022import org.openstreetmap.josm.command.AddPrimitivesCommand;
023import org.openstreetmap.josm.command.ChangePropertyCommand;
024import org.openstreetmap.josm.command.ChangePropertyKeyCommand;
025import org.openstreetmap.josm.command.Command;
026import org.openstreetmap.josm.command.DeleteCommand;
027import org.openstreetmap.josm.data.coor.LatLon;
028import org.openstreetmap.josm.data.osm.OsmPrimitive;
029import org.openstreetmap.josm.data.validation.OsmValidator;
030import org.openstreetmap.josm.data.validation.Severity;
031import org.openstreetmap.josm.data.validation.Test;
032import org.openstreetmap.josm.data.validation.TestError;
033import org.openstreetmap.josm.tools.LanguageInfo;
034import org.openstreetmap.josm.tools.Logging;
035
036/**
037 * Class to write a collection of validator errors out to XML.
038 * The format is inspired by the
039 * <a href="https://wiki.openstreetmap.org/wiki/Osmose#Issues_file_format">Osmose API issues file format</a>
040 * @since 12667
041 */
042public class ValidatorErrorWriter extends XmlWriter {
043
044    /**
045     * Constructs a new {@code ValidatorErrorWriter} that will write to the given {@link PrintWriter}.
046     * @param out PrintWriter to write XML to
047     */
048    public ValidatorErrorWriter(PrintWriter out) {
049        super(out);
050    }
051
052    /**
053     * Constructs a new {@code ValidatorErrorWriter} that will write to a given {@link OutputStream}.
054     * @param out OutputStream to write XML to
055     */
056    public ValidatorErrorWriter(OutputStream out) {
057        super(new PrintWriter(new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))));
058    }
059
060    /**
061     * Write validator errors to designated output target
062     * @param validationErrors Test error collection to write
063     * @throws IOException in case of I/O error
064     */
065    public void write(Collection<TestError> validationErrors) throws IOException {
066        Set<Test> analysers = validationErrors.stream().map(TestError::getTester)
067                .sorted(Comparator.comparing(t -> t.getSource().toString())).collect(Collectors.toCollection(LinkedHashSet::new));
068        String timestamp = Instant.now().toString();
069
070        out.println("<?xml version='1.0' encoding='UTF-8'?>");
071        out.println("<analysers generator='JOSM' timestamp='"+timestamp+"'>");
072
073        try (OsmWriter osmWriter = OsmWriterFactory.createOsmWriter(out, true, OsmChangeBuilder.DEFAULT_API_VERSION)) {
074            String lang = LanguageInfo.getJOSMLocaleCode();
075
076            for (Test test : analysers) {
077                out.println("  <analyser timestamp='" + timestamp + "' name='" + XmlWriter.encode(test.getName()) + "'>");
078                // Build map of test error classes for the current test
079                Map<ErrorClass, List<TestError>> map = new HashMap<>();
080                for (Entry<Severity, Map<String, Map<String, List<TestError>>>> e1 :
081                        OsmValidator.getErrorsBySeverityMessageDescription(validationErrors, e -> e.getTester() == test).entrySet()) {
082                    for (Entry<String, Map<String, List<TestError>>> e2 : e1.getValue().entrySet()) {
083                        ErrorClass errorClass = new ErrorClass(e1.getKey(), e2.getKey());
084                        List<TestError> list = map.computeIfAbsent(errorClass, k -> new ArrayList<>());
085                        e2.getValue().values().forEach(list::addAll);
086                    }
087                }
088                // Write classes
089                for (ErrorClass ec : map.keySet()) {
090                    out.println("    <class id='" + ec.id + "' level='" + ec.severity.getLevel() + "'>");
091                    out.println("      <classtext lang='" + XmlWriter.encode(lang) + "' title='" + XmlWriter.encode(ec.message) + "'/>");
092                    out.println("    </class>");
093                }
094
095                // Write errors
096                for (Entry<ErrorClass, List<TestError>> entry : map.entrySet()) {
097                    for (TestError error : entry.getValue()) {
098                        LatLon ll = error.getPrimitives().iterator().next().getBBox().getCenter();
099                        out.println("    <error class='" + entry.getKey().id + "'>");
100                        out.print("      <location");
101                        osmWriter.writeLatLon(ll);
102                        out.println("/>");
103                        for (OsmPrimitive p : error.getPrimitives()) {
104                            out.print("    ");
105                            p.accept(osmWriter);
106                        }
107                        out.println("      <text lang='" + XmlWriter.encode(lang) +
108                                "' value='" + XmlWriter.encode(error.getDescription()) + "'/>");
109                        if (error.isFixable()) {
110                            out.println("      <fixes>");
111                            Command fix = error.getFix();
112                            if (fix instanceof AddPrimitivesCommand) {
113                                Logging.info("TODO: {0}", fix);
114                            } else if (fix instanceof DeleteCommand) {
115                                Logging.info("TODO: {0}", fix);
116                            } else if (fix instanceof ChangePropertyCommand) {
117                                Logging.info("TODO: {0}", fix);
118                            } else if (fix instanceof ChangePropertyKeyCommand) {
119                                Logging.info("TODO: {0}", fix);
120                            } else {
121                                Logging.warn("Unsupported command type: {0}", fix);
122                            }
123                            out.println("      </fixes>");
124                        }
125                        out.println("    </error>");
126                    }
127                }
128
129                out.println("  </analyser>");
130            }
131
132            out.println("</analysers>");
133            out.flush();
134        }
135    }
136
137    private static class ErrorClass {
138        static int idCounter;
139        final Severity severity;
140        final String message;
141        final int id;
142
143        ErrorClass(Severity severity, String message) {
144            this.severity = severity;
145            this.message = message;
146            this.id = ++idCounter;
147        }
148    }
149}