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().stream().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}