Ticket #25021: #25021-Fixed-Sonar-issues-in-VectorDataSet.patch

File #25021-Fixed-Sonar-issues-in-VectorDataSet.patch, 10.5 KB (added by DanProgs <Abenteurer.2901@…>, 2 days ago)
  • src/org/openstreetmap/josm/data/vector/VectorDataSet.java

    ### Eclipse Workspace Patch 1.0
    #P josm
    diff --git src/org/openstreetmap/josm/data/vector/VectorDataSet.java src/org/openstreetmap/josm/data/vector/VectorDataSet.java
    index 86d5383..7988be8 100644
     
    252252
    253253    @Override
    254254    public <T extends VectorPrimitive> Collection<T> getPrimitives(Predicate<? super VectorPrimitive> predicate) {
    255         Collection<VectorPrimitive> primitives = tryRead(this.readWriteLock, () -> {
     255        final Collection<VectorPrimitive> primitives = tryRead(this.readWriteLock, () -> {
    256256            final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
    257257            final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
    258258            return Stream.concat(dataStoreStream, Stream.of(this.customDataStore))
    … …  
    363363    public Collection<VectorPrimitive> getAllSelected() {
    364364        return tryRead(this.readWriteLock, () -> {
    365365            final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
    366             Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
    367                 return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getPrimitivesMap)
    368                   .flatMap(dataMap -> {
    369                     // Synchronize on dataMap to avoid concurrent modification errors
    370                     synchronized (dataMap) {
    371                         return this.currentSelectedPrimitives.stream().map(dataMap::get).filter(Objects::nonNull);
    372                     }
    373                 }).collect(Collectors.toList());
     366            final Stream<VectorDataStore> dataStoreStream = dataStore != null
     367                    ? dataStore.stream().map(MVTTile::getData)
     368                    : Stream.empty();
     369
     370            return Stream.concat(dataStoreStream, Stream.ofNullable(this.customDataStore))
     371                    .map(VectorDataStore::getPrimitivesMap)
     372                    .filter(Objects::nonNull)
     373                    .flatMap(dataMap -> {
     374                        final Map<PrimitiveId, VectorPrimitive> lockObject = dataMap;
     375                        synchronized (lockObject) {
     376                            final List<VectorPrimitive> selectedInMap = new ArrayList<>();
     377                            for (PrimitiveId id : this.currentSelectedPrimitives) {
     378                                final VectorPrimitive primitive = lockObject.get(id);
     379                                if (primitive != null) {
     380                                    selectedInMap.add(primitive);
     381                                }
     382                            }
     383                            return selectedInMap.stream();
     384                        }
     385                    })
     386                    .collect(Collectors.toList());
    374387        }).orElseGet(Collections::emptyList);
    375388    }
    376389
    … …  
    490503    private boolean doSelectionChange(final Function<Set<VectorPrimitive>,
    491504            SelectionChangeEvent<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet>> command) {
    492505        synchronized (this.selectionLock) {
    493             SelectionChangeEvent<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> event =
     506            final SelectionChangeEvent<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> event =
    494507                    command.apply(currentSelectedPrimitives.stream().map(this::getPrimitiveById).collect(Collectors.toSet()));
    495508            if (event.isNop()) {
    496509                return false;
    … …  
    635648     * @param invisibleLayers The layer to not show
    636649     */
    637650    public void setInvisibleLayers(Collection<String> invisibleLayers) {
    638         String[] currentInvisibleLayers = invisibleLayers.stream().filter(Objects::nonNull).toArray(String[]::new);
    639         List<String> temporaryList = Arrays.asList(currentInvisibleLayers);
     651        final String[] currentInvisibleLayers = invisibleLayers.stream().filter(Objects::nonNull).toArray(String[]::new);
     652        final List<String> temporaryList = Arrays.asList(currentInvisibleLayers);
    640653        this.dataStoreMap.values().stream().flatMap(Collection::stream).map(MVTTile::getData)
    641654          .forEach(dataStore -> dataStore.getAllPrimitives().parallelStream()
    642655            .forEach(primitive -> primitive.setVisible(!temporaryList.contains(primitive.getLayer()))));
  • test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java

    diff --git test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
    index 39f1b65..9b66b6b 100644
     
    66
    77import java.nio.file.Paths;
    88import java.util.ArrayList;
     9import java.util.Arrays;
    910import java.util.Collection;
    1011import java.util.Collections;
    1112import java.util.HashSet;
     13import java.util.List;
     14import java.util.concurrent.CountDownLatch;
     15import java.util.concurrent.ExecutorService;
     16import java.util.concurrent.Executors;
     17import java.util.concurrent.Future;
     18import java.util.concurrent.TimeUnit;
     19import java.util.stream.Collectors;
    1220
    1321import org.awaitility.Awaitility;
    1422import org.awaitility.Durations;
    1523import org.junit.jupiter.api.BeforeEach;
    1624import org.junit.jupiter.api.RepeatedTest;
     25import org.junit.jupiter.api.Test;
    1726import org.openstreetmap.josm.TestUtils;
     27import org.openstreetmap.josm.data.coor.LatLon;
    1828import org.openstreetmap.josm.data.imagery.ImageryInfo;
    1929import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
    2030import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorCachedTileLoader;
    … …  
    7383            throw new IllegalArgumentException("Tiles come with a {z}, {x}, and {y} component");
    7484        }
    7585        final MapboxVectorTileSource tileSource = layer.getTileSource();
    76         MapboxVectorCachedTileLoader tileLoader = layer.getTileLoader();
    77         Collection<MVTTile> tilesCollection = new ArrayList<>();
     86        final MapboxVectorCachedTileLoader tileLoader = layer.getTileLoader();
     87        final Collection<MVTTile> tilesCollection = new ArrayList<>();
    7888        for (int i = 0; i < tiles.length / 3; i++) {
    7989            final MVTTile tile = (MVTTile) layer.createTile(tileSource, tiles[3 * i + 1], tiles[3 * i + 2], tiles[3 * i]);
    8090            tileLoader.createTileLoaderJob(tile).submit();
    … …  
    124134        assertEquals(1, dataSet.getWays().size());
    125135        assertEquals(0, dataSet.getRelations().size());
    126136    }
     137   
     138   
     139    /**
     140     * Test that {@link VectorDataSet#getAllSelected()} returns the expected selected primitives
     141     * and returns an empty collection once the selection is cleared.
     142     */
     143    @Test
     144    void testGetAllSelected() {
     145        final VectorDataSet dataSet = this.layer.getData();
     146        assertTrue(dataSet.getAllSelected().isEmpty());
     147
     148        dataSet.setZoom(14);
     149        loadTile(this.layer, 14, 3248, 6258);
     150
     151        // Select two arbitrary nodes from the loaded tile
     152        final List<VectorNode> nodesToSelect = dataSet.getNodes().stream()
     153                .limit(2)
     154                .collect(Collectors.toList());
     155        assertEquals(2, nodesToSelect.size());
     156
     157        dataSet.setSelected(nodesToSelect);
     158
     159        final Collection<VectorPrimitive> selected = dataSet.getAllSelected();
     160        assertEquals(2, selected.size());
     161        assertTrue(selected.containsAll(nodesToSelect));
     162
     163        // Clear selection again
     164        dataSet.clearSelection();
     165        assertTrue(dataSet.getAllSelected().isEmpty());
     166    }
     167
     168    /**
     169     * Test that {@link VectorDataSet#getAllSelected()} properly combines selected primitives
     170     * from both loaded vector tiles and the custom data store.
     171     */
     172    @Test
     173    void testGetAllSelectedWithCustomDataStore() {
     174        final VectorDataSet dataSet = this.layer.getData();
     175        dataSet.setZoom(14);
     176        loadTile(this.layer, 14, 3248, 6258);
     177
     178        // Combine a primitive from the tile and a custom primitive (customDataStore)
     179        final VectorPrimitive tilePrimitive = dataSet.getNodes().iterator().next();
     180        final VectorNode customNode = new VectorNode("custom-layer");
     181        customNode.setCoor(new LatLon(0, 0));
     182        dataSet.addPrimitive(customNode);
     183
     184        dataSet.setSelected(Arrays.asList(tilePrimitive, customNode));
     185
     186        final Collection<VectorPrimitive> selected = dataSet.getAllSelected();
     187        assertEquals(2, selected.size());
     188        assertTrue(selected.contains(tilePrimitive));
     189        assertTrue(selected.contains(customNode));
     190    }
     191
     192    /**
     193     * Test that {@link VectorDataSet#getAllSelected()} is thread-safe when primitives are
     194     * concurrently added and removed while reading the current selection.
     195     *
     196     * @throws Exception if any thread execution or timeout error occurs
     197     */
     198    @Test
     199    void testGetAllSelectedThreadSafety() throws Exception {
     200        final VectorDataSet dataSet = this.layer.getData();
     201        dataSet.setZoom(14);
     202        loadTile(this.layer, 14, 3248, 6258);
     203
     204        final List<VectorNode> nodesToSelect = dataSet.getNodes().stream()
     205                .limit(5)
     206                .collect(Collectors.toList());
     207        dataSet.setSelected(nodesToSelect);
     208
     209        final int iterations = 200;
     210        final ExecutorService executor = Executors.newFixedThreadPool(2);
     211        final CountDownLatch startLatch = new CountDownLatch(1);
     212
     213        // Thread 1: Continuously add and remove primitives (modifies underlying maps)
     214        final Future<?> writer = executor.submit(() -> {
     215            startLatch.await();
     216            for (int i = 0; i < iterations; i++) {
     217                final VectorNode tempNode = new VectorNode("temp-layer");
     218                tempNode.setCoor(new LatLon(i * 0.001, i * 0.001));
     219                dataSet.addPrimitive(tempNode);
     220                dataSet.removePrimitive(tempNode);
     221            }
     222            return null;
     223        });
     224
     225        // Thread 2: Concurrently read getAllSelected()
     226        final Future<?> reader = executor.submit(() -> {
     227            startLatch.await();
     228            for (int i = 0; i < iterations; i++) {
     229                final Collection<VectorPrimitive> selected = dataSet.getAllSelected();
     230                assertEquals(5, selected.size());
     231            }
     232            return null;
     233        });
     234
     235        startLatch.countDown();
     236        writer.get(5, TimeUnit.SECONDS);
     237        reader.get(5, TimeUnit.SECONDS);
     238        executor.shutdown();
     239    }
    127240}