001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import java.text.MessageFormat; 005import java.util.ArrayList; 006import java.util.Collection; 007import java.util.Collections; 008import java.util.HashSet; 009import java.util.List; 010import java.util.Map; 011import java.util.Optional; 012import java.util.Set; 013import java.util.stream.Collectors; 014 015import org.openstreetmap.josm.command.Command; 016import org.openstreetmap.josm.data.coor.LatLon; 017import org.openstreetmap.josm.data.osm.DataSet; 018import org.openstreetmap.josm.data.osm.OsmPrimitive; 019import org.openstreetmap.josm.data.osm.OsmUtils; 020import org.openstreetmap.josm.data.validation.TestError; 021import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory; 022import org.openstreetmap.josm.gui.mappaint.mapcss.ExpressionFactory; 023import org.openstreetmap.josm.gui.mappaint.mapcss.LiteralExpression; 024import org.openstreetmap.josm.gui.mappaint.mapcss.Selector; 025import org.openstreetmap.josm.tools.DefaultGeoProperty; 026import org.openstreetmap.josm.tools.GeoProperty; 027import org.openstreetmap.josm.tools.GeoPropertyIndex; 028import org.openstreetmap.josm.tools.Logging; 029import org.openstreetmap.josm.tools.Territories; 030 031/** 032 * Utility class for checking rule assertions of {@link MapCSSTagCheckerRule}. 033 */ 034final class MapCSSTagCheckerAsserts { 035 036 private MapCSSTagCheckerAsserts() { 037 // private constructor 038 } 039 040 private static final ArrayList<MapCSSTagCheckerRule> previousChecks = new ArrayList<>(); 041 042 /** 043 * Checks that rule assertions are met for the given set of TagChecks. 044 * @param check The TagCheck for which assertions have to be checked 045 * @param assertions The assertions to check (map values correspond to expected result) 046 * @param assertionConsumer The handler for assertion error messages 047 */ 048 static void checkAsserts(final MapCSSTagCheckerRule check, final Map<String, Boolean> assertions, 049 final MapCSSTagChecker.AssertionConsumer assertionConsumer) { 050 final DataSet ds = new DataSet(); 051 Logging.debug("Check: {0}", check); 052 for (final Map.Entry<String, Boolean> i : assertions.entrySet()) { 053 Logging.debug("- Assertion: {0}", i); 054 final OsmPrimitive p = OsmUtils.createPrimitive(i.getKey(), getLocation(check), true); 055 // Build minimal ordered list of checks to run to test the assertion 056 List<Set<MapCSSTagCheckerRule>> checksToRun = new ArrayList<>(); 057 Set<MapCSSTagCheckerRule> checkDependencies = getTagCheckDependencies(check, previousChecks); 058 if (!checkDependencies.isEmpty()) { 059 checksToRun.add(checkDependencies); 060 } 061 checksToRun.add(Collections.singleton(check)); 062 // Add primitive to dataset to avoid DataIntegrityProblemException when evaluating selectors 063 ds.addPrimitiveRecursive(p); 064 final Collection<TestError> pErrors = MapCSSTagChecker.getErrorsForPrimitive(p, true, checksToRun); 065 Logging.debug("- Errors: {0}", pErrors); 066 final boolean isError = pErrors.stream().anyMatch(e -> e.getTester() instanceof MapCSSTagChecker.MapCSSTagCheckerAndRule 067 && ((MapCSSTagChecker.MapCSSTagCheckerAndRule) e.getTester()).rule.equals(check.rule)); 068 if (isError != i.getValue()) { 069 assertionConsumer.accept(MessageFormat.format("Expecting test ''{0}'' (i.e., {1}) to {2} {3} (i.e., {4})", 070 check.getMessage(p), check.rule.selectors, i.getValue() ? "match" : "not match", i.getKey(), p.getKeys())); 071 } 072 if (isError) { 073 // Check that autofix works as expected 074 Command fix = check.fixPrimitive(p); 075 if (fix != null && fix.executeCommand() && !MapCSSTagChecker.getErrorsForPrimitive(p, true, checksToRun).isEmpty()) { 076 assertionConsumer.accept(MessageFormat.format("Autofix does not work for test ''{0}'' (i.e., {1})", 077 check.getMessage(p), check.rule.selectors)); 078 } 079 } 080 ds.removePrimitive(p); 081 } 082 previousChecks.add(check); 083 } 084 085 public static void clear() { 086 previousChecks.clear(); 087 previousChecks.trimToSize(); 088 } 089 090 private static LatLon getLocation(MapCSSTagCheckerRule check) { 091 Optional<String> inside = getFirstInsideCountry(check); 092 if (inside.isPresent()) { 093 GeoPropertyIndex<Boolean> index = Territories.getGeoPropertyIndex(inside.get()); 094 if (index != null) { 095 GeoProperty<Boolean> prop = index.getGeoProperty(); 096 if (prop instanceof DefaultGeoProperty) { 097 return ((DefaultGeoProperty) prop).getRandomLatLon(); 098 } 099 } 100 } 101 return LatLon.ZERO; 102 } 103 104 private static Optional<String> getFirstInsideCountry(MapCSSTagCheckerRule check) { 105 return check.rule.selectors.stream() 106 .filter(s -> s instanceof Selector.GeneralSelector) 107 .flatMap(s -> ((Selector.GeneralSelector) s).getConditions().stream()) 108 .filter(c -> c instanceof ConditionFactory.ExpressionCondition) 109 .map(c -> ((ConditionFactory.ExpressionCondition) c).getExpression()) 110 .filter(c -> c instanceof ExpressionFactory.IsInsideFunction) 111 .map(c -> (ExpressionFactory.IsInsideFunction) c) 112 .map(ExpressionFactory.IsInsideFunction::getArg) 113 .filter(e -> e instanceof LiteralExpression) 114 .map(e -> ((LiteralExpression) e).getLiteral()) 115 .filter(l -> l instanceof String) 116 .map(l -> ((String) l).split(",", -1)[0]) 117 .findFirst(); 118 } 119 120 /** 121 * Returns the set of tagchecks on which this check depends on. 122 * @param check the tagcheck 123 * @param schecks the collection of tagchecks to search in 124 * @return the set of tagchecks on which this check depends on 125 * @since 7881 126 */ 127 private static Set<MapCSSTagCheckerRule> getTagCheckDependencies(MapCSSTagCheckerRule check, 128 Collection<MapCSSTagCheckerRule> schecks) { 129 Set<MapCSSTagCheckerRule> result = new HashSet<>(); 130 Set<String> classes = check.rule.selectors.stream() 131 .filter(s -> s instanceof Selector.AbstractSelector) 132 .flatMap(s -> ((Selector.AbstractSelector) s).getConditions().stream()) 133 .filter(c -> c instanceof ConditionFactory.ClassCondition) 134 .map(c -> ((ConditionFactory.ClassCondition) c).id) 135 .collect(Collectors.toSet()); 136 if (schecks != null && !classes.isEmpty()) { 137 return schecks.stream() 138 .filter(tc -> !check.equals(tc)) 139 .filter(tc -> tc.setClassExpressions.stream().anyMatch(classes::contains)) 140 .collect(Collectors.toSet()); 141 } 142 return result; 143 } 144}