/* (c) 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.test.onlineTest;
import com.mongodb.BasicDBObject;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.ServerAddress;
import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.Difference;
import org.custommonkey.xmlunit.DifferenceListener;
import org.custommonkey.xmlunit.examples.RecursiveElementNameAndTextQualifier;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogBuilder;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.impl.DataStoreInfoImpl;
import org.geoserver.catalog.impl.NamespaceInfoImpl;
import org.geoserver.catalog.impl.WorkspaceInfoImpl;
import org.geoserver.data.test.SystemTestData;
import org.geoserver.test.GeoServerSystemTestSupport;
import org.geoserver.util.IOUtils;
import org.geotools.feature.NameImpl;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assume.assumeTrue;
/**
* Tests the integration between MongoDB and App-schema. This test are integration tests hence they
* require a MongoDB instance. If no fixture file for MongoDB exists these tests will be skipped.
*/
public final class ComplexMongoDBTest extends GeoServerSystemTestSupport {
private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(ComplexMongoDBTest.class);
private static final Path ROOT_DIRECTORY = createTempDir();
private static File APP_SCHEMA_MAPPINGS;
private static final String STATIONS_DATA_BASE_NAME = UUID.randomUUID().toString();
private static final String STATIONS_COLLECTION_NAME = "stations";
private static MongoClient MONGO_CLIENT;
@BeforeClass
public static void setup() throws Exception {
// check that a fixture file was provided
File fixtureFile = getFixtureFile();
if (!fixtureFile.exists()) {
// create fixture example file
createFixtureExample(fixtureFile);
LOGGER.warning(String.format(
"No fixture file '%s' for MongoDB exists, example file created. Tests will eb skipped.",
fixtureFile.getAbsolutePath()));
}
assumeTrue(fixtureFile.exists());
// load MongoDB connection properties from fixture file
Properties properties = loadFixtureProperties(fixtureFile);
// check that we have access to a mongodb and instantiate the client
String hostAsString = properties.getProperty("mongo.host", "127.0.0.1");
String portAsString = properties.getProperty("mongo.port", "27017");
ServerAddress serverAddress = new ServerAddress(hostAsString, Integer.parseInt(portAsString));
MONGO_CLIENT = new MongoClient(serverAddress,
new MongoClientOptions.Builder().serverSelectionTimeout(2000).build());
try {
MONGO_CLIENT.listDatabaseNames().first();
} catch (Exception exception) {
// not able to connect to the MongoDB database
throw new RuntimeException(String.format(
"Could not connect to MongoDB database with host '%s' and port '%s'.",
hostAsString, portAsString));
}
// moving schemas files to the test directory
moveResourceToTempDir("/schemas/stations.xsd", "stations.xsd");
// copy the mappings file and do some substitutions
APP_SCHEMA_MAPPINGS = moveResourceToTempDir("/mappings/stations.xml", "stations.xml");
String mappingsContent = new String(Files.readAllBytes(APP_SCHEMA_MAPPINGS.toPath()));
mappingsContent = mappingsContent.replaceAll("\\{dataBaseName\\}", STATIONS_DATA_BASE_NAME);
mappingsContent = mappingsContent.replaceAll("\\{collectionName\\}", STATIONS_COLLECTION_NAME);
mappingsContent = mappingsContent.replaceAll("\\{mongoHost\\}", hostAsString);
mappingsContent = mappingsContent.replaceAll("\\{mongoPort\\}", portAsString);
mappingsContent = mappingsContent.replaceAll("\\{schemaStore\\}",
new File(ROOT_DIRECTORY.toFile(), "schema-store").getAbsolutePath());
Files.write(APP_SCHEMA_MAPPINGS.toPath(), mappingsContent.getBytes());
// insert stations data set in MongoDB
File stationsFile1 = moveResourceToTempDir("/data/stations1.json", "stations1.json");
File stationsFile2 = moveResourceToTempDir("/data/stations2.json", "stations2.json");
String stationsContent1 = new String(Files.readAllBytes(stationsFile1.toPath()));
String stationsContent2 = new String(Files.readAllBytes(stationsFile2.toPath()));
insertJson(STATIONS_DATA_BASE_NAME, STATIONS_COLLECTION_NAME, stationsContent1);
insertJson(STATIONS_DATA_BASE_NAME, STATIONS_COLLECTION_NAME, stationsContent2);
}
@AfterClass
public static void tearDown() throws Exception {
// remove the temporary directory
if (ROOT_DIRECTORY != null) {
IOUtils.delete(ROOT_DIRECTORY.toFile());
}
// remove test data base from MongoDB
try {
MONGO_CLIENT.getDatabase(STATIONS_DATA_BASE_NAME).drop();
} catch (Exception exception) {
// ignore any error just log it
LOGGER.log(Level.WARNING, "Error removing test database from MongoDB.", exception);
}
}
@Override
protected void onSetUp(SystemTestData testData) throws Exception {
super.onSetUp(testData);
Catalog catalog = getCatalog();
// create necessary stations workspace
WorkspaceInfoImpl workspace = new WorkspaceInfoImpl();
workspace.setName("st");
NamespaceInfoImpl nameSpace = new NamespaceInfoImpl();
nameSpace.setPrefix("st");
nameSpace.setURI("http://www.stations.org/1.0");
catalog.add(workspace);
catalog.add(nameSpace);
// create the app-schema data store
Map<String, Serializable> params = new HashMap<>();
params.put("dbtype", "app-schema");
params.put("url", "file:" + APP_SCHEMA_MAPPINGS.getAbsolutePath());
DataStoreInfoImpl dataStore = new DataStoreInfoImpl(getCatalog());
dataStore.setName(UUID.randomUUID().toString());
dataStore.setType("app-schema");
dataStore.setConnectionParameters(params);
dataStore.setWorkspace(workspace);
dataStore.setEnabled(true);
catalog.add(dataStore);
// build the feature type for the root mapping (StationFeature)
CatalogBuilder builder = new CatalogBuilder(catalog);
builder.setStore(dataStore);
builder.setWorkspace(workspace);
FeatureTypeInfo featureType = builder.buildFeatureType(
new NameImpl(nameSpace.getURI(), "StationFeature"));
catalog.add(featureType);
LayerInfo layer = builder.buildLayer(featureType);
catalog.add(layer);
}
@Test
public void testGetStationFeatures() throws Exception {
Document result = getAsDOM("wfs?request=GetFeature&version=1.1.0&typename=st:StationFeature");
checkResult(result, "/results/result1.xml");
}
@Test
public void testGetStationFeaturesWithFilter() throws Exception {
String postContent = readResourceContent("/querys/postQuery1.xml");
Document result = postAsDOM("wfs?request=GetFeature&version=1.1.0&typename=st:StationFeature", postContent);
checkResult(result, "/results/result2.xml");
}
/**
* Load MongoDB connection properties.
*/
private static Properties loadFixtureProperties(File fixtureFile) {
Properties properties = new Properties();
try (InputStream input = new FileInputStream(fixtureFile)) {
// load properties from fixture file
properties.load(input);
return properties;
} catch (Exception exception) {
throw new RuntimeException(String.format(
"Error reading fixture file '%s'.",
fixtureFile.getAbsolutePath()), exception);
}
}
/**
* Write fixture example file for MongoDB, if the file already
* exists nothing will be done.
*/
private static void createFixtureExample(File fixtureFile) {
// example fixture file
File exampleFixtureFile = new File(fixtureFile.getAbsolutePath() + ".example");
if (exampleFixtureFile.exists()) {
// file already exists
return;
}
// default MongoDB connection parameters
Properties properties = new Properties();
properties.put("mongo.host", "127.0.0.1");
properties.put("mongo.port", "27017");
try (OutputStream output = new FileOutputStream(exampleFixtureFile)) {
properties.store(output, "This is an example fixture. Update the values " +
"and remove the .example suffix to enable the test");
} catch (Exception exception) {
throw new RuntimeException(String.format(
"Error writing example fixture file '%s'.",
fixtureFile.getAbsolutePath()), exception);
}
}
/**
* Gets the fixture file for MongoDB, parent directories are created if needed.
*/
private static File getFixtureFile() {
File directory = new File(System.getProperty("user.home") + File.separator + ".geoserver");
if (!directory.exists()) {
// make sure parent directory exists
directory.mkdir();
}
return new File(directory, "mongodb.properties");
}
/**
* Helper method that reads the content of a resource to a string.
*/
private static String readResourceContent(String resourcePath) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (InputStream input = ComplexMongoDBTest.class.getResourceAsStream(resourcePath)) {
IOUtils.copy(input, output);
return new String(output.toByteArray());
} catch (Exception exception) {
throw new RuntimeException(String.format(
"Error reading resource '%s' content.",
resourcePath), exception);
}
}
/**
* Helper method that will check the returned document against a
* control document. The control document will be parsed from the
* provided resource.
*/
private void checkResult(Document result, String resultResourcePath) throws Exception {
// parse the expected result document
Document expected = dom(ComplexMongoDBTest.class.getResourceAsStream(resultResourcePath), true);
Diff diff = new Diff(expected, result);
// elements don't need to be in the same order
diff.overrideElementQualifier(new RecursiveElementNameAndTextQualifier());
// ignore timestamps and schema locations differences
diff.overrideDifferenceListener(new AppSchemaDifferenceListener());
Assert.assertThat(diff.similar(), is(true));
}
/**
* Helper method that creates a temporary directory taking
* care of the IO exception.
*/
private static Path createTempDir() {
try {
return Files.createTempDirectory("app-schema-mongo");
} catch (Exception exception) {
throw new RuntimeException("Error creating temporary directory.", exception);
}
}
/**
* Helper method that moves a resource to the tests temporary directory
* and return the resource file path.
*/
private static File moveResourceToTempDir(String resourcePath, String resourceName) {
// create the output file
File outputFile = new File(ROOT_DIRECTORY.toFile(), resourceName);
try (InputStream input = ComplexMongoDBTest.class.getResourceAsStream(resourcePath);
OutputStream output = new FileOutputStream(outputFile)) {
// copy the resource content to the output file
IOUtils.copy(input, output);
} catch (Exception exception) {
throw new RuntimeException("Error moving resource to temporary directory.", exception);
}
return outputFile;
}
/**
* Helper method that reads a JSON object from a file and inserts it in
* the provided database and collection.
*/
private static void insertJson(String databaseName, String collectionName, String json) {
// insert stations data
org.bson.Document document = org.bson.Document.parse(json);
MONGO_CLIENT.getDatabase(databaseName).getCollection(collectionName).insertOne(document);
// add / update geometry index
BasicDBObject indexObject = new BasicDBObject();
indexObject.put("geometry", "2dsphere");
MONGO_CLIENT.getDatabase(databaseName).getCollection(collectionName).createIndex(indexObject);
}
/**
* Helper listener for ignoring differences related with timestamp
* values and schema locations.
*/
private static final class AppSchemaDifferenceListener implements DifferenceListener {
@Override
public int differenceFound(Difference difference) {
String controlNode = difference.getControlNodeDetail().getNode().getLocalName();
String testNode = difference.getTestNodeDetail().getNode().getLocalName();
if (controlNode != null && controlNode.equals(testNode) && (
controlNode.equalsIgnoreCase("timestamp") ||
controlNode.equalsIgnoreCase("schemaLocation"))) {
// ignore this difference
return RETURN_IGNORE_DIFFERENCE_NODES_SIMILAR;
}
// valid difference, the engine may try to match a node in a different order
return RETURN_ACCEPT_DIFFERENCE;
}
@Override
public void skippedComparison(Node node, Node node1) {
}
}
}