/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2011, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.map;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.crs.DefaultEngineeringCRS;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Tests for concurrent use of MapContent.
*
* @author Michael Bedward
* @source $URL$
* @version $Id$
*/
public class MapContentConcurrencyTest {
private static final ReferencedEnvelope WORLD =
new ReferencedEnvelope(0, 100, 0, 100, DefaultEngineeringCRS.GENERIC_2D);
// timeout period for waiting listener (see end of class)
private static final long LISTENER_TIMEOUT = 500;
private static final ExecutorService executor = Executors.newCachedThreadPool();
private MapContent mapContent;
private WaitingMapListener listener;
@Before
public void setup() {
mapContent = new MapContent();
listener = new WaitingMapListener();
mapContent.addMapLayerListListener(listener);
}
@Test
public void addingLayersOnSeparateThreads() throws Exception {
final CountDownLatch startLatch = new CountDownLatch(1);
Layer layer1 = new MockLayer(WORLD);
Layer layer2 = new MockLayer(WORLD);
// Add each layer twice. If all goes well, the list should get
// a aingle instance of each layer and a single layer-added event
// should be fired for each
executor.submit(new AddLayerTask(layer1, startLatch));
executor.submit(new AddLayerTask(layer1, startLatch));
executor.submit(new AddLayerTask(layer2, startLatch));
executor.submit(new AddLayerTask(layer2, startLatch));
listener.setExpected(WaitingMapListener.Type.ADDED, 2);
startLatch.countDown();
listener.await(WaitingMapListener.Type.ADDED, LISTENER_TIMEOUT);
assertEquals(2, mapContent.layers().size());
}
/**
* In this test we create multiple tasks: half of which add a layer to
* the layer list and the rest which remove the layer. Then the tasks are
* shuffled, submitted to the executor, and all started at the same time
* (or at least given permission to run at the same time).
*
* @throws Exception
*/
@Test
public void addAndRemoveOnSeparateThreads() throws Exception {
final CountDownLatch startLatch = new CountDownLatch(1);
final int numThreads = 100;
Layer layer1 = new MockLayer(WORLD);
List<Runnable> tasks = new ArrayList<Runnable>(numThreads);
int k = 0;
while (k < numThreads / 2) {
tasks.add(new AddLayerTask(layer1, startLatch) {
@Override
public void postRun() {
assertTrue(mapContent.layers().size() == 1);
}
});
k++ ;
}
while (k < numThreads) {
tasks.add(new RemoveLayerTask(layer1, startLatch) {
@Override
public void postRun() {
assertTrue(mapContent.layers().isEmpty());
}
});
k++ ;
}
Collections.shuffle(tasks);
for (Runnable task : tasks) {
executor.submit(task);
}
// Counting down the latch allows the tasks to run
startLatch.countDown();
}
/*
* Waits for a latch to be zeroed then adds a layer to the map content.
*/
private class AddLayerTask implements Runnable {
private final CountDownLatch startLatch;
private final Layer layer;
public AddLayerTask(Layer layer, CountDownLatch startLatch) {
this.layer = layer;
this.startLatch = startLatch;
}
@Override
public void run() {
try {
startLatch.await();
preRun();
mapContent.layers().add(layer);
postRun();
} catch (InterruptedException ex) { /* empty */ }
}
public void preRun() {}
public void postRun() {}
}
/*
* Waits for a latch to be zeroed then adds a layer to the map content.
*/
private class RemoveLayerTask implements Runnable {
private final CountDownLatch startLatch;
private final Layer layer;
public RemoveLayerTask(Layer layer, CountDownLatch startLatch) {
this.layer = layer;
this.startLatch = startLatch;
}
@Override
public void run() {
try {
startLatch.await();
preRun();
mapContent.layers().remove(layer);
postRun();
} catch (InterruptedException ex) { /* empty */ }
}
public void preRun() {}
public void postRun() {}
}
}