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}