/*
* Copyright (c) 2014 Oculus Info Inc. http://www.oculusinfo.com/
*
* Released under the MIT License.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.oculusinfo.annotation.rest;
import com.oculusinfo.annotation.AnnotationData;
import com.oculusinfo.annotation.filter.AnnotationFilter;
import com.oculusinfo.annotation.impl.JSONAnnotation;
import com.oculusinfo.annotation.index.AnnotationIndexer;
import com.oculusinfo.annotation.index.impl.AnnotationIndexerImpl;
import com.oculusinfo.annotation.init.DefaultAnnotationFilterFactoryProvider;
import com.oculusinfo.annotation.init.DefaultAnnotationIOFactoryProvider;
import com.oculusinfo.annotation.init.providers.StandardAnnotationFilterFactoryProvider;
import com.oculusinfo.annotation.init.providers.StandardAnnotationIOFactoryProvider;
import com.oculusinfo.annotation.io.AnnotationIO;
import com.oculusinfo.annotation.io.serialization.AnnotationSerializer;
import com.oculusinfo.annotation.io.serialization.JSONAnnotationDataSerializer;
import com.oculusinfo.annotation.util.AnnotationGenerator;
import com.oculusinfo.binning.TileIndex;
import com.oculusinfo.binning.io.DefaultPyramidIOFactoryProvider;
import com.oculusinfo.binning.io.PyramidIO;
import com.oculusinfo.binning.io.serialization.DefaultTileSerializerFactoryProvider;
import com.oculusinfo.binning.io.serialization.TileSerializer;
import com.oculusinfo.factory.providers.FactoryProvider;
import com.oculusinfo.tile.init.providers.StandardImageRendererFactoryProvider;
import com.oculusinfo.tile.init.providers.StandardLayerConfigurationProvider;
import com.oculusinfo.tile.init.providers.StandardPyramidIOFactoryProvider;
import com.oculusinfo.tile.init.providers.StandardTilePyramidFactoryProvider;
import com.oculusinfo.tile.init.providers.StandardTileSerializerFactoryProvider;
import com.oculusinfo.tile.init.providers.StandardTileTransformerFactoryProvider;
import com.oculusinfo.tile.rendering.LayerConfiguration;
import com.oculusinfo.tile.rest.config.ConfigException;
import com.oculusinfo.tile.rest.config.ConfigService;
import com.oculusinfo.tile.rest.layer.LayerService;
import com.oculusinfo.tile.rest.layer.LayerServiceImpl;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class AnnotationServiceTests {
private static final Logger LOGGER = LoggerFactory.getLogger( AnnotationServiceTests.class );
private static final String UNIT_TEST_CONFIG_JSON = "unit-test-config.json";
static final int NUM_THREADS = 8;
static final double [] BOUNDS = { 180, 85.05, -180, -85.05 };
static final int NUM_ENTRIES = 50;
protected AnnotationService _service;
protected String _layerId = "test-layer0";
protected String _dataId;
protected String [] _groups;
protected LayerService _layerService;
private ConfigService _configService;
List<AnnotationWrapper> _publicAnnotations = new ArrayList<>();
Integer _remainingAnnotations = NUM_ENTRIES * NUM_THREADS;
Random _random = new Random( System.currentTimeMillis() );
final Object decisionLock = new Object();
@Before
public void setup () {
try {
String configFile = "res:///" + UNIT_TEST_CONFIG_JSON;
Set<FactoryProvider<PyramidIO>> tileIoSet = new HashSet<>();
tileIoSet.addAll( Arrays.asList( DefaultPyramidIOFactoryProvider.values() ) );
Set<FactoryProvider<AnnotationIO>> annotationIoSet = new HashSet<>();
annotationIoSet.addAll( Arrays.asList( DefaultAnnotationIOFactoryProvider.values() ) );
Set<FactoryProvider<TileSerializer<?>>> serializerSet = new HashSet<>();
serializerSet.addAll( Arrays.asList( DefaultTileSerializerFactoryProvider.values() ) );
Set<FactoryProvider<AnnotationFilter>> filterIoSet = new HashSet<>();
filterIoSet.addAll( Arrays.asList( DefaultAnnotationFilterFactoryProvider.values() ) );
FactoryProvider<LayerConfiguration> layerConfigurationProvider = new StandardLayerConfigurationProvider(
new StandardPyramidIOFactoryProvider( tileIoSet ),
new StandardTilePyramidFactoryProvider(),
new StandardTileSerializerFactoryProvider(serializerSet),
new StandardImageRendererFactoryProvider(),
new StandardTileTransformerFactoryProvider()
);
_configService = mock(ConfigService.class);
withMockConfigService();
_layerService = new LayerServiceImpl( configFile, layerConfigurationProvider, _configService);
_dataId = _layerService.getLayerConfiguration( _layerId, null ).getPropertyValue( LayerConfiguration.DATA_ID );
JSONArray groupsJson = _layerService.getLayerConfiguration( _layerId, null ).getPropertyValue( AnnotationServiceImpl.GROUPS );
_groups = new String[ groupsJson.length() ];
for (int i=0;i<groupsJson.length();i++){
_groups[i] = groupsJson.getString( i );
}
AnnotationIndexer annotationIndexer = new AnnotationIndexerImpl();
AnnotationSerializer annotationSerializer = new JSONAnnotationDataSerializer();
_service = new AnnotationServiceImpl( _layerService,
annotationSerializer,
annotationIndexer,
new StandardAnnotationIOFactoryProvider( annotationIoSet ),
new StandardAnnotationFilterFactoryProvider( filterIoSet ) );
} catch (Exception e) {
LOGGER.error( "Error setting up test", e );
}
}
private void withMockConfigService() throws URISyntaxException, IOException, ConfigException {
File configFile = new File(this.getClass().getClassLoader().getResource(UNIT_TEST_CONFIG_JSON).toURI());
String configFileContent = new String(Files.readAllBytes(Paths.get(configFile.getPath())), StandardCharsets.UTF_8);
when(_configService.replaceProperties(any(File.class))).thenReturn(configFileContent);
}
@After
public void teardown () {
_service = null;
}
public synchronized AnnotationWrapper getRandomPublicAnnotation() {
if ( _publicAnnotations.size() == 0 ) {
return null;
}
int index = _random.nextInt( _publicAnnotations.size() );
return _publicAnnotations.get( index );
}
public synchronized void addAnnotationToPublic( AnnotationWrapper annotation ) {
_publicAnnotations.add( annotation );
}
public synchronized void removeAnnotationFromPublic( AnnotationWrapper annotation ) {
_publicAnnotations.remove( annotation );
_remainingAnnotations--;
}
public synchronized int getRemainingAnnotations() {
return _remainingAnnotations;
}
private class AnnotationWrapper {
private AnnotationData<?> _data;
AnnotationWrapper( AnnotationData<?> data ) {
_data = data;
}
public synchronized AnnotationData<?> clone() {
JSONObject json = _data.toJSON();
return JSONAnnotation.fromJSON( json );
}
public synchronized void update(AnnotationData<?> newState ) {
_data = newState;
}
}
private class Tester implements Runnable {
private String _name;
private List<AnnotationWrapper> _annotations = new LinkedList<>();
Tester( String name ) {
// set thread name
_name = name;
AnnotationGenerator generator = new AnnotationGenerator( BOUNDS, _groups );
// generate private local annotations
for ( int i=0; i<NUM_ENTRIES; i++ ) {
_annotations.add( new AnnotationWrapper( generator.generateJSONAnnotation() ) );
}
}
public void run() {
while ( true ) {
int operation;
AnnotationWrapper randomAnnotation;
// decision process is atomic
synchronized( decisionLock ) {
if ( getRemainingAnnotations() == 0 && _annotations.size() == 0 ) {
// no more annotations, we are done
return;
}
// get a random annotation
randomAnnotation = getRandomPublicAnnotation();
if ( randomAnnotation == null ) {
// no available public annotations
if ( _annotations.size() > 0 ) {
// local is available
operation = _random.nextInt(2) + 2; // 2-3 read or write
} else {
// nothing available at the moment
operation = -1;
}
} else {
// public annotations available
if ( _annotations.size() > 0 ) {
// public and local both available
operation = _random.nextInt(4); // 0-3 modify, remove, read or write
} else {
// only public available
operation = _random.nextInt(3); // 0-2 modify, remove, or read
}
}
}
switch ( operation ) {
case 0:
// modify
modify( randomAnnotation );
break;
case 1:
// remove
remove( randomAnnotation );
break;
case 2:
// read
read();
break;
case 3:
// write
int index = _random.nextInt( _annotations.size() );
AnnotationWrapper annotation = _annotations.get( index );
_annotations.remove( index );
write( annotation );
break;
default:
break;
}
}
}
private void write( AnnotationWrapper annotation ) {
AnnotationData<?> clone = annotation.clone();
long start = System.currentTimeMillis();
_service.write( _layerId, annotation.clone() );
long end = System.currentTimeMillis();
double time = ((end-start)/1000.0);
LOGGER.debug( "Thread " + _name + " successfully wrote " + clone.getUUID() + " in " + time + " sec" );
addAnnotationToPublic( annotation );
}
private void read() {
TileIndex tile = getRandomTile();
long start = System.currentTimeMillis();
List<AnnotationData<?>> scan = readTile( tile );
long end = System.currentTimeMillis();
double time = ((end-start)/1000.0);
LOGGER.debug( "Thread " + _name + " read " + scan.size() + " entries from " + tile.getLevel() + ", " + tile.getX() + ", " + tile.getY() + " in " + time + " sec" );
}
private void modify( AnnotationWrapper annotation ) {
AnnotationData<?> oldAnnotation = annotation.clone();
AnnotationData<?> newAnnotation = editAnnotation( oldAnnotation );
try {
long start = System.currentTimeMillis();
_service.modify( _layerId, newAnnotation );
long end = System.currentTimeMillis();
double time = ((end-start)/1000.0);
annotation.update( newAnnotation );
LOGGER.debug( "Thread " + _name + " successfully modified " + newAnnotation.getUUID() + " in " + time + " sec" );
} catch (Exception e) {
LOGGER.debug( "Thread " + _name + " unsuccessfully modified " + newAnnotation.getUUID() );
}
}
private void remove( AnnotationWrapper annotation ) {
AnnotationData<?> clone = annotation.clone();
try {
long start = System.currentTimeMillis();
_service.remove( _layerId, clone.getCertificate() );
long end = System.currentTimeMillis();
double time = ((end-start)/1000.0);
removeAnnotationFromPublic(annotation);
LOGGER.debug("Thread " + _name + " successfully removed " + clone.getUUID() + " in " + time + " sec");
} catch (Exception e) {
LOGGER.debug("Thread " + _name + " unsuccessfully removed " + clone.getUUID() );
}
}
private AnnotationData<?> editAnnotation( AnnotationData<?> annotation ) {
JSONObject json = annotation.toJSON();
AnnotationGenerator generator = new AnnotationGenerator( BOUNDS, _groups );
try {
int type = (int)(Math.random() * 2);
switch (type) {
case 0:
// change position
double [] xy = generator.randomPosition();
json.put("x", xy[0]);
json.put("y", xy[1]);
break;
default:
// change data
JSONObject data = new JSONObject();
data.put("comment", generator.randomComment() );
json.put("data", data);
break;
}
} catch ( Exception e ) {
e.printStackTrace();
}
return JSONAnnotation.fromJSON(json);
}
}
@Ignore
@Test
public void concurrentTest() {
try {
/*
This test is designed to mimic a high user write / modify / read / remove traffic.
All test threads begin with a list of annotations that will be written. Once an annotation
is written, its existence becomes public and any other thread may read / modify / remove it.
*/
long start = System.currentTimeMillis();
List<Thread> threads = new LinkedList<>();
// write / read
for (int i = 0; i < NUM_THREADS; i++) {
Thread t = new Thread(new Tester("" + i));
threads.add(t);
t.start();
}
for (Thread t : threads) {
try {
t.join();
} catch (Exception e) {
e.printStackTrace();
}
}
// ensure everything was removed
List<AnnotationData<?>> scan = readAll();
Assert.assertTrue(scan.size() == 0);
long end = System.currentTimeMillis();
double time = ((end - start) / 1000.0);
LOGGER.debug("Completed in " + time + " seconds");
} finally {
try {
LayerConfiguration config = _layerService.getLayerConfiguration( _layerId, null );
config.produce( PyramidIO.class );
config.produce( AnnotationIO.class );
LOGGER.debug("Deleting temporary file system folders");
try {
File testDir = new File( ".\\" + _dataId );
for ( File f : testDir.listFiles() ) {
f.delete();
}
testDir.delete();
} catch ( Exception e ) {
// swallow exception
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private List<AnnotationData<?>> readTile( TileIndex tile ) {
List<AnnotationData<?>> annotations = new ArrayList<>();
List<List<AnnotationData<?>>> data = _service.read( _layerId, tile, null );
if ( data != null ) {
for ( List<AnnotationData<?>> bin : data ) {
for ( AnnotationData<?> annotation : bin ) {
annotations.add( annotation );
}
}
Assert.assertTrue( validateTile( annotations ) );
}
return annotations;
}
private List<AnnotationData<?>> readAll() {
// scan all
return readTile( new TileIndex( 0, 0, 0 ) );
}
private boolean validateTile( List<AnnotationData<?>> annotations ) {
for ( int i=0; i<annotations.size(); i++ ) {
for ( int j=i+1; j<annotations.size(); j++ ) {
if ( annotations.get(i) == annotations.get(j) ) {
// duplicate found
LOGGER.error( "Duplicate instance of annotation found in same tile" );
return false;
}
}
}
return true;
}
private TileIndex getRandomTile() {
final int MAX_DEPTH = 4;
int level = (int)(Math.random() * MAX_DEPTH);
int x = (int)(Math.random() * (level * (1 << level)) );
int y = (int)(Math.random() * (level * (1 << level)) );
return new TileIndex( level, x, y, AnnotationIndexer.NUM_BINS, AnnotationIndexer.NUM_BINS );
}
}