Changeset 19195 in josm for trunk


Ignore:
Timestamp:
2024-08-14T19:44:54+02:00 (3 months ago)
Author:
taylor.smock
Message:

Fix #23860: Duplicate key+value in preset causes an ISE in TagChecker

This fixes the actual issue in TagChecker, but also adds a sanity check to
TaggingPresetPreferenceTestIT since it is usually unintended to have
duplicate key/values (and it is always a problem if they are different and are
Key items).

The fix for TagChecker is just keeping whatever value is last. Not ideal, but
it should work 99% of the time since an object won't match the preset if we have
highway=footway and highway=footway2 as Key objects.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/resources/data/defaultpresets.xml

    r19193 r19195  
    32733273                <check key="ferry:cable" text="Reaction ferry" />
    32743274                <space />
    3275                 <reference ref="oh" />
    32763275                <reference ref="public_transport_route_optionals" />
    32773276                <reference ref="pt_route_opt2" />
     
    38093808            </checkgroup>
    38103809            <combo key="sanitary_dump_station" text="Dump Station" values="yes,public,customers,no" />
    3811             <reference ref="wheelchair" />
    38123810            <reference ref="internet" />
    38133811            <space />
     
    69836981                <combo key="generator:type" text="Generator Type" values_searchable="true">
    69846982                    <list_entry value="bioreactor" short_description="gasification" />
     6983                    <list_entry value="boiler" short_description="" />
    69856984                    <list_entry value="pyrolysis" short_description="" />
    69866985                    <list_entry value="reciprocating_engine" short_description="combustion" />
    69876986                    <list_entry value="steam_generator" short_description="combustion" />
    69886987                </combo>
    6989                 <combo key="generator:type" text="Generator Type" values="bioreactor,boiler,reciprocating_engine,steam_generator" />
    69906988                <reference ref="power_output" />
    69916989            </item> <!-- Waste Power Generator -->
  • trunk/src/org/openstreetmap/josm/data/validation/tests/TagChecker.java

    r19172 r19195  
    719719            Map<String, String> matchingTags = tp.data.stream()
    720720                    .filter(i -> Boolean.TRUE.equals(i.matches(tags)))
    721                     .filter(i -> i instanceof KeyedItem).map(i -> ((KeyedItem) i).key)
    722                     .collect(Collectors.toMap(k -> k, tags::get));
     721                    .filter(KeyedItem.class::isInstance).map(i -> ((KeyedItem) i).key)
     722                    .collect(Collectors.toMap(k -> k, tags::get, (o, n) -> n));
    723723            if (matchingPresetsOK.stream().noneMatch(
    724724                    tp2 -> matchingTags.entrySet().stream().allMatch(
  • trunk/test/unit/org/openstreetmap/josm/data/validation/tests/TagCheckerTest.java

    r19172 r19195  
    77
    88import java.io.IOException;
     9import java.util.Collections;
    910import java.util.List;
    1011import java.util.function.Consumer;
     
    1819import org.openstreetmap.josm.data.osm.OsmUtils;
    1920import org.openstreetmap.josm.data.osm.Tag;
     21import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
    2022import org.openstreetmap.josm.data.validation.Severity;
    2123import org.openstreetmap.josm.data.validation.TestError;
     24import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
     25import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
     26import org.openstreetmap.josm.gui.tagging.presets.items.Key;
     27import org.openstreetmap.josm.spi.preferences.Config;
    2228import org.openstreetmap.josm.testutils.annotations.I18n;
    2329import org.openstreetmap.josm.testutils.annotations.TaggingPresets;
     
    415421        assertEquals(0, errors.size());
    416422    }
     423
     424    /**
     425     * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/23860">Bug #23860</a>.
     426     * Duplicate key
     427     * @throws IOException if any I/O error occurs
     428     */
     429    @Test
     430    void testTicket23860Equal() throws IOException {
     431        ValidatorPrefHelper.PREF_OTHER.put(true);
     432        Config.getPref().putBoolean(TagChecker.PREF_CHECK_PRESETS_TYPES, true);
     433        final TaggingPreset originalBusStop = org.openstreetmap.josm.gui.tagging.presets.TaggingPresets.getMatchingPresets(
     434                Collections.singleton(TaggingPresetType.NODE), Collections.singletonMap("highway", "bus_stop"), false)
     435                .iterator().next();
     436        final Key duplicateKey = new Key();
     437        duplicateKey.key = "highway";
     438        duplicateKey.value = "bus_stop";
     439        try {
     440            originalBusStop.data.add(duplicateKey);
     441            final List<TestError> errors = test(OsmUtils.createPrimitive("way highway=bus_stop"));
     442            assertEquals(1, errors.size());
     443        } finally {
     444            originalBusStop.data.remove(duplicateKey);
     445        }
     446    }
     447
     448    /**
     449     * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/23860">Bug #23860</a>.
     450     * Duplicate key
     451     * @throws IOException if any I/O error occurs
     452     */
     453    @Test
     454    void testTicket23860NonEqual() throws IOException {
     455        ValidatorPrefHelper.PREF_OTHER.put(true);
     456        Config.getPref().putBoolean(TagChecker.PREF_CHECK_PRESETS_TYPES, true);
     457        final TaggingPreset originalBusStop = org.openstreetmap.josm.gui.tagging.presets.TaggingPresets.getMatchingPresets(
     458                        Collections.singleton(TaggingPresetType.NODE), Collections.singletonMap("highway", "bus_stop"), false)
     459                .iterator().next();
     460        final Key duplicateKey = new Key();
     461        duplicateKey.key = "highway";
     462        duplicateKey.value = "bus_stop2";
     463        try {
     464            originalBusStop.data.add(duplicateKey);
     465            final List<TestError> errors = test(OsmUtils.createPrimitive("way highway=bus_stop"));
     466            assertEquals(0, errors.size());
     467        } finally {
     468            originalBusStop.data.remove(duplicateKey);
     469        }
     470    }
    417471}
  • trunk/test/unit/org/openstreetmap/josm/gui/preferences/map/TaggingPresetPreferenceTestIT.java

    r19004 r19195  
    1111import java.util.ArrayList;
    1212import java.util.Collection;
     13import java.util.HashMap;
    1314import java.util.HashSet;
    1415import java.util.List;
    1516import java.util.Locale;
     17import java.util.Objects;
    1618import java.util.Set;
    1719import java.util.concurrent.TimeUnit;
     20import java.util.stream.Collectors;
     21import java.util.stream.Stream;
    1822
    1923import org.junit.jupiter.api.BeforeAll;
     
    2529import org.openstreetmap.josm.gui.preferences.AbstractExtendedSourceEntryTestCase;
    2630import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
     31import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
    2732import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
    2833import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetsTest;
     34import org.openstreetmap.josm.gui.tagging.presets.items.Check;
     35import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
     36import org.openstreetmap.josm.gui.tagging.presets.items.Key;
     37import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
    2938import org.openstreetmap.josm.gui.tagging.presets.items.Link;
    3039import org.openstreetmap.josm.spi.preferences.Config;
     
    3544import org.openstreetmap.josm.tools.ImageProvider;
    3645import org.openstreetmap.josm.tools.Logging;
     46import org.openstreetmap.josm.tools.Utils;
    3747import org.xml.sax.SAXException;
    3848
     
    107117        TaggingPresetsTest.waitForIconLoading(presets);
    108118        // check that links are correct and not redirections
    109         presets.parallelStream().flatMap(x -> x.data.stream().filter(i -> i instanceof Link).map(i -> ((Link) i).getUrl())).forEach(u -> {
     119        presets.parallelStream().flatMap(x -> x.data.stream().filter(Link.class::isInstance).map(i -> ((Link) i).getUrl())).forEach(u -> {
    110120            try {
    111121                Response cr = HttpClient.create(new URL(u), "HEAD").setMaxRedirects(-1).connect();
     
    121131            }
    122132        });
     133        presets.parallelStream().flatMap(TaggingPresetPreferenceTestIT::checkForDuplicates)
     134                .filter(Objects::nonNull)
     135                .forEach(message -> addOrIgnoreError(source, messages, message, ignoredErrors));
    123136        Collection<String> errorsAndWarnings = Logging.getLastErrorAndWarnings();
    124137        boolean error = false;
     
    141154        }
    142155    }
     156
     157    /**
     158     * Look for duplicate key/value objects
     159     * @param preset to check
     160     * @return The messages to print to console for fixing
     161     */
     162    private static Stream<String> checkForDuplicates(TaggingPreset preset) {
     163        final HashMap<String, List<KeyedItem>> dupMap = preset.data.stream()
     164                .flatMap(TaggingPresetPreferenceTestIT::getKeyedItems)
     165                .collect(Collectors.groupingBy(i -> i.key, HashMap::new, Collectors.toCollection(ArrayList::new)));
     166        dupMap.values().forEach(TaggingPresetPreferenceTestIT::removeUnnecessaryDuplicates);
     167        dupMap.values().removeIf(l -> l.size() <= 1);
     168        if (!dupMap.isEmpty()) {
     169            final StringBuilder prefixBuilder = new StringBuilder();
     170            if (preset.group != null && preset.group.name != null) {
     171                prefixBuilder.append(preset.group.name).append('/');
     172            }
     173            if (preset.name != null) {
     174                prefixBuilder.append(preset.name).append('/');
     175            }
     176            final String prefix = prefixBuilder.toString();
     177            return dupMap.keySet().stream().map(k -> "Duplicate key: " + prefix + k);
     178        }
     179        return Stream.empty();
     180    }
     181
     182    /**
     183     * Remove keys that are technically duplicates, but are otherwise OK due to working around limitations of the XML.
     184     * @param l The list of keyed items to look through
     185     */
     186    private static void removeUnnecessaryDuplicates(List<KeyedItem> l) {
     187        // Remove keys that are "truthy" when a check will be on or off. This seems to be used for setting defaults in chunks.
     188        // We might want to extend chunks to have child `<key>` elements which will set default values for the chunk.
     189        ArrayList<KeyedItem> toRemove = new ArrayList<>(Math.min(4, l.size() / 10));
     190        for (Key first : Utils.filteredCollection(l, Key.class)) {
     191            for (Check second : Utils.filteredCollection(l, Check.class)) {
     192                if (second.value_off.equals(first.value) || second.value_on.equals(first.value)) {
     193                    toRemove.add(first);
     194                }
     195            }
     196        }
     197        l.removeAll(toRemove);
     198    }
     199
     200    /**
     201     * Convert an item to a collection of items (needed for {@link CheckGroup})
     202     * @param item The item to convert
     203     * @return The {@link KeyedItem}s to use
     204     */
     205    private static Stream<? extends KeyedItem> getKeyedItems(TaggingPresetItem item) {
     206        // We care about cases where a preset has two separate hardcoded values
     207        // Check should use default="on|off" and value_(on|off) to control the default.
     208        if (item instanceof Key || item instanceof Check) {
     209            return Stream.of((KeyedItem) item);
     210        } else if (item instanceof CheckGroup) {
     211            return ((CheckGroup) item).checks.stream();
     212        }
     213        return Stream.empty();
     214    }
    143215}
Note: See TracChangeset for help on using the changeset viewer.