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}