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