/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.test;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import org.geoserver.data.CatalogWriter;
import org.geoserver.data.test.MockData;
import org.geoserver.data.test.SystemTestData;
import org.geoserver.data.util.IOUtils;
import org.geoserver.test.onlineTest.setup.AppSchemaTestOracleSetup;
import org.geoserver.test.onlineTest.setup.AppSchemaTestPostgisSetup;
import org.geoserver.test.onlineTest.support.AbstractReferenceDataSetup;
import org.geotools.data.complex.AppSchemaDataAccessTest;
import org.geotools.xml.resolver.SchemaCatalog;
import com.vividsolutions.jts.geom.Envelope;
/**
* Abstract base class for mock data based on the app-schema test data set.
*
* @author Ben Caradoc-Davies, CSIRO Exploration and Mining
*/
public abstract class AbstractAppSchemaMockData extends SystemTestData
implements NamespaceTestData {
/**
* Folder for for test data.
*/
private static final String TEST_DATA = "/test-data/";
/**
* Prefix for gsml namespace.
*/
public static final String GSML_PREFIX = "gsml";
/**
* URI for gsml namespace.
*/
public static final String GSML_URI = "urn:cgi:xmlns:CGI:GeoSciML:2.0";
/**
* Schema location URL for the the top-level gsml XSD.
*/
public static final String GSML_SCHEMA_LOCATION_URL = "http://www.geosciml.org/geosciml/2.0/xsd/geosciml.xsd";
/**
* PRefix for spec namespace.
*/
public static final String SPEC_PREFIX = "spec";
/**
* Map of namespace prefix to namespace URI for GML 32 schema.
*/
@SuppressWarnings("serial")
protected static final Map<String, String> GML32_NAMESPACES = Collections
.unmodifiableMap(new TreeMap<String, String>() {
{
put("cgu", "urn:cgi:xmlns:CGI:Utilities:3.0.0");
put("gco", "http://www.isotc211.org/2005/gco");
put("gmd", "http://www.isotc211.org/2005/gmd");
put("gml", "http://www.opengis.net/gml/3.2");
put("gsml", "urn:cgi:xmlns:CGI:GeoSciML-Core:3.0.0");
put("sa", "http://www.opengis.net/sampling/2.0");
put("spec", "http://www.opengis.net/samplingSpecimen/2.0");
put("swe", "http://www.opengis.net/swe/1.0/gml32");
put("wfs", "http://www.opengis.net/wfs/2.0");
put("xlink", "http://www.w3.org/1999/xlink");
}
});
/**
* Map of namespace prefix to namespace URI.
*/
@SuppressWarnings("serial")
private static final Map<String, String> NAMESPACES = Collections
.unmodifiableMap(new LinkedHashMap<String, String>() {
{
put(GSML_PREFIX, GSML_URI);
put("gml", "http://www.opengis.net/gml");
put("xlink", "http://www.w3.org/1999/xlink");
put("sa", "http://www.opengis.net/sampling/1.0");
put("om", "http://www.opengis.net/om/1.0");
put("cv", "http://www.opengis.net/cv/0.2.1");
put("swe", "http://www.opengis.net/swe/1.0.1");
put("sml", "http://www.opengis.net/sensorML/1.0.1");
}
});
/**
* Use FeatureTypeInfo constants for srs handling as values
*/
private static final String KEY_SRS_HANDLINGS = "srsHandling";
/**
* The feature type alias, a string
*/
private static final String KEY_ALIAS = "alias";
/**
* The style name
*/
private static final String KEY_STYLE = "style";
/**
* The srs code (a number) for this layer
*/
private static final String KEY_SRS_NUMBER = "srs";
/**
* The lon/lat envelope as a JTS Envelope
*/
private static final String KEY_LL_ENVELOPE = "ll_envelope";
/**
* The native envelope as a JTS Envelope
*/
private static final String KEY_NATIVE_ENVELOPE = "native_envelope";
private static final Envelope DEFAULT_ENVELOPE = new Envelope(-180, 180, -90, 90);
/**
* Map of data store name to data store connection parameters map.
*/
private final Map<String, Map<String, Serializable>> datastoreParams = new LinkedHashMap<String, Map<String, Serializable>>();
/**
* Map of data store name to namespace prefix.
*/
private final Map<String, String> datastoreNamespacePrefixes = new LinkedHashMap<String, String>();
private final Map<String, String> namespaces;
private final Map<String, String> layerStyles = new LinkedHashMap<String,String>();
private File styles;
/** the 'featureTypes' directory, under 'data' */
private File featureTypesBaseDir;
/**
* Pair of property file name and feature type directory to create db tables for online tests
*/
private Map<String, File> propertiesFiles;
/**
* Indicates fixture id (postgis or oracle) if running in online mode
*/
private String onlineTestId;
/**
* SchemaCatalog to work with AppSchemaValidator for test requests validation.
*/
private SchemaCatalog catalog;
/**
* True if running 3D online test. Only matters for Oracle, since a special wkt parser is needed.
*/
private boolean is3D = false;
/**
* Constructor with the default namespaces, schema directory, and catalog file.
*/
public AbstractAppSchemaMockData() {
this(NAMESPACES);
}
static File newRandomDirectory() {
try {
return IOUtils.createRandomDirectory("target", "app-schema-mock", "data");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public AbstractAppSchemaMockData(Map<String, String> namespaces) {
super(newRandomDirectory());
this.namespaces = new LinkedHashMap<String, String>(namespaces);
// create a featureTypes directory
featureTypesBaseDir = new File(data, "featureTypes");
featureTypesBaseDir.mkdir();
// create the styles directory
styles = new File(data, "styles");
styles.mkdir();
propertiesFiles = new HashMap<String, File>();
addContent();
// create corresponding tables in the test db using the properties files
if (!propertiesFiles.isEmpty()) {
try {
createTablesInTestDatabase();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
setUpCatalog();
}
public boolean isOracleOnlineTest() {
return "oracle".equals(onlineTestId);
}
public boolean isPostgisOnlineTest() {
return "postgis".equals(onlineTestId);
}
/**
*
* @param catalogLocation
* file location relative to test-data dir.
*/
protected void setSchemaCatalog(String catalogLocation) {
if (catalogLocation != null) {
URL resolvedCatalogLocation = getClass().getResource(TEST_DATA + catalogLocation);
if (resolvedCatalogLocation == null) {
throw new RuntimeException(
"Test catalog location must be relative to test-data directory!");
}
this.catalog = SchemaCatalog.build(resolvedCatalogLocation);
}
}
public SchemaCatalog getSchemaCatalog() {
return catalog;
}
/**
* Return the namespace prefixx to namespace URI map for this data.
*
* @see org.geoserver.test.NamespaceTestData#getNamespaces()
*/
public Map<String, String> getNamespaces() {
return Collections.unmodifiableMap(namespaces);
}
/**
* Subclasses must override this method to add namespaces with
* {@link #putNamespace(String, String)} and feature types with
* {@link #addFeatureType(String, String, String, String...)}.
*/
protected abstract void addContent();
/**
* Copy a file from the test-data directory to a feature type directory. if fileName contains
* directory path eg, dir1/dir2/file.xml, the full path will be used to locate the resource.
* After which the directory will be ignored.
*
* @param namespacePrefix
* namespace prefix of the WFS feature type
* @param typeName
* local name of the WFS feature type
* @param fileName
* short name of the file in test-data to copy
* @param data
* mock data root directory
*/
private void copyFileToFeatureTypeDir(String namespacePrefix, String typeName, String fileName) {
copy(AppSchemaDataAccessTest.class.getResourceAsStream(TEST_DATA + fileName),
"featureTypes" + "/" + getDataStoreName(namespacePrefix, typeName) + "/"
+ fileName.substring(fileName.lastIndexOf("/") + 1, fileName.length()));
}
/**
* Returns the root of the mock data directory,
*
* @see org.geoserver.data.test.TestData#getDataDirectoryRoot()
*/
public File getDataDirectoryRoot() {
return data;
}
/**
* Returns true.
*
* @see org.geoserver.data.test.TestData#isTestDataAvailable()
*/
public boolean isTestDataAvailable() {
return true;
}
/**
* Configures mock data directory.
*
* @see org.geoserver.data.test.TestData#setUp()
*/
public void setUp() throws IOException {
setUpCatalog();
setUpSecurity();
copy(MockData.class.getResourceAsStream("services.xml"), "services.xml");
}
@Override
public void setUpDefault() throws Exception {
//do nothing
}
/**
* Removes the mock data directory.
*
* @see org.geoserver.data.test.TestData#tearDown()
*/
public void tearDown() {
try {
IOUtils.delete(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
data = null;
}
/**
* Writes catalog.xml to the data directory.
*/
private void setUpCatalog() {
CatalogWriter writer = new CatalogWriter();
writer.dataStores(datastoreParams, datastoreNamespacePrefixes, Collections
.<String> emptySet());
writer.coverageStores(new HashMap<String, Map<String, String>>(),
new HashMap<String, String>(), Collections.<String> emptySet());
writer.namespaces(namespaces);
writer.styles(layerStyles);
try {
writer.write(new File(data, "catalog.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Copies from an {@link InputStream} to path under the mock data directory.
*
* @param input
* source from which file content is copied
* @param location
* path relative to mock data directory
*/
private void copy(InputStream input, String location) {
try {
IOUtils.copy(input, new File(data, location));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Copies a String content to a file in the path under the mock data directory.
*
* @param content
* file content
* @param location
* path relative to mock data directory
*/
private void copy(String content, String location) {
File file = new File(data, location);
FileWriter writer = null;
try {
writer = new FileWriter(file);
writer.write(content);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
/**
* Write an info.xml file describing a feature type to the feature type directory.
*
* <p>
*
* Stolen from {@link MockData}.
*
* @param namespacePrefix
* namespace prefix of the WFS feature type
* @param typeName
* namespace prefix of the WFS feature type
* @param featureTypeDir
* feature type directory
* @param dataStoreName
* data store directory name
*/
private static void writeInfoFile(String namespacePrefix, String typeName, File featureTypeDir,
String dataStoreName) {
// prepare extra params default
Map<String, Object> params = new HashMap<String, Object>();
params.put(KEY_STYLE, "Default");
params.put(KEY_SRS_HANDLINGS, 2);
params.put(KEY_ALIAS, null);
Integer srs = 4326;
params.put(KEY_SRS_NUMBER, srs);
try {
featureTypeDir.mkdir();
File info = new File(featureTypeDir, "info.xml");
info.delete();
info.createNewFile();
FileWriter writer = new FileWriter(info);
writer.write("<featureType datastore=\"" + dataStoreName + "\">");
writer.write("<name>" + typeName + "</name>");
if (params.get(KEY_ALIAS) != null)
writer.write("<alias>" + params.get(KEY_ALIAS) + "</alias>");
writer.write("<SRS>" + params.get(KEY_SRS_NUMBER) + "</SRS>");
// this mock type may have wrong SRS compared to the actual one in the property files...
// let's configure SRS handling not to alter the original one, and have 4326 used only
// for capabilities
writer.write("<SRSHandling>" + params.get(KEY_SRS_HANDLINGS) + "</SRSHandling>");
writer.write("<title>" + typeName + "</title>");
writer.write("<abstract>abstract about " + typeName + "</abstract>");
writer.write("<numDecimals value=\"8\"/>");
writer.write("<keywords>" + typeName + "</keywords>");
Envelope llEnvelope = (Envelope) params.get(KEY_LL_ENVELOPE);
if (llEnvelope == null)
llEnvelope = DEFAULT_ENVELOPE;
writer.write("<latLonBoundingBox dynamic=\"false\" minx=\"" + llEnvelope.getMinX()
+ "\" miny=\"" + llEnvelope.getMinY() + "\" maxx=\"" + llEnvelope.getMaxX()
+ "\" maxy=\"" + llEnvelope.getMaxY() + "\"/>");
Envelope nativeEnvelope = (Envelope) params.get(KEY_NATIVE_ENVELOPE);
if (nativeEnvelope != null)
writer.write("<nativeBBox dynamic=\"false\" minx=\"" + nativeEnvelope.getMinX()
+ "\" miny=\"" + nativeEnvelope.getMinY() + "\" maxx=\""
+ nativeEnvelope.getMaxX() + "\" maxy=\"" + nativeEnvelope.getMaxY()
+ "\"/>");
String style = (String) params.get(KEY_STYLE);
if (style == null)
style = "Default";
writer.write("<styles default=\"" + style + "\"/>");
writer.write("</featureType>");
writer.flush();
writer.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Build the connection parameters map for a data store.
*
* @param namespacePrefix
* namespace prefix of the WFS feature type
* @param typeName
* local name of the WFS feature type
* @param mappingFileName
* file name of the app-schema mapping file
* @param featureTypesBaseDir
* feature types base directory
* @param dataStoreName
* data store name
* @return
*/
@SuppressWarnings("serial")
private static Map<String, Serializable> buildAppSchemaDatastoreParams(
final String namespacePrefix, final String typeName, final String mappingFileName,
final File featureTypesBaseDir, final String dataStoreName) {
try {
return new LinkedHashMap<String, Serializable>() {
{
put("dbtype", "app-schema");
put("url", new File(new File(featureTypesBaseDir, dataStoreName),
mappingFileName).toURI().toURL());
}
};
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
/**
* Add one feature type, copying its resources and registering, creating its info.xml, and
* adding it to catalog.xml.
*
* @param namespacePrefix
* namespace prefix of the WFS feature type
* @param typeName
* local name of the WFS feature type
* @param mappingFileName
* file name of the app-schema mapping file
* @param supportFileNames
* names of other files to be copied into the feature type directory
*/
public void addFeatureType(String namespacePrefix, String typeName, String mappingFileName,
String... supportFileNames) {
File featureTypeDir = getFeatureTypeDir(featureTypesBaseDir, namespacePrefix, typeName);
String dataStoreName = getDataStoreName(namespacePrefix, typeName);
try {
writeInfoFile(namespacePrefix, typeName, featureTypeDir, dataStoreName);
copyMappingAndSupportFiles(namespacePrefix, typeName, mappingFileName, supportFileNames);
// if mappingFileName contains directory, eg, dir1/dir2/file.xml, we will ignore the
// directory from here on
addDataStore(dataStoreName, namespacePrefix, buildAppSchemaDatastoreParams(
namespacePrefix, typeName, mappingFileName.substring(mappingFileName
.lastIndexOf("/") + 1, mappingFileName.length()), featureTypesBaseDir,
dataStoreName));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* The same as {@link #addFeatureType(String, String, String, String...)} except this to enable 3D WKT parser for Oracle.
* Use this one for tests with 3D data that needs to be run online.
*
* @param namespacePrefix
* namespace prefix of the WFS feature type
* @param typeName
* local name of the WFS feature type
* @param mappingFileName
* file name of the app-schema mapping file
* @param supportFileNames
* names of other files to be copied into the feature type directory
*/
public void add3DFeatureType(String namespacePrefix, String typeName, String mappingFileName,
String... supportFileNames) {
addFeatureType(namespacePrefix, typeName, mappingFileName, supportFileNames);
this.is3D = true;
}
/**
* Determine which setup class to use based on the fixture id specified in the vm arg.
*
* @throws Exception
*/
private void createTablesInTestDatabase() throws Exception {
AbstractReferenceDataSetup setup = null;
if (isOracleOnlineTest()) {
if (is3D) {
setup = AppSchemaTestOracleSetup.get3DInstance(propertiesFiles);
} else {
setup = AppSchemaTestOracleSetup.getInstance(propertiesFiles);
}
// Run the sql script through setup
setup.setUp();
setup.tearDown();
} else if (isPostgisOnlineTest()) {
setup = AppSchemaTestPostgisSetup.getInstance(propertiesFiles);
// Run the sql script through setup
setup.setUp();
setup.tearDown();
}
}
/**
* Adds the specified style to the data directory
* @param styleId the style id
* @param filename filename of SLD file in test-data to be copied into the data directory
* @throws IOException
*/
public void addStyle(String styleId, String fileName) {
layerStyles.put(styleId, styleId + ".sld");
InputStream styleContents = getClass().getResourceAsStream(TEST_DATA + fileName);
File to = new File(styles, styleId + ".sld");
try {
IOUtils.copy(styleContents, to);
} catch (IOException e) {
throw new RuntimeException (e);
}
}
/**
* Add a datastore and record its prefix in the lookup table.
*
* @param dataStoreName
* @param namespacePrefix
* @param params
*/
private void addDataStore(String dataStoreName, String namespacePrefix,
Map<String, Serializable> params) {
datastoreParams.put(dataStoreName, params);
datastoreNamespacePrefixes.put(dataStoreName, namespacePrefix);
}
/**
* Put a namespace into the map.
*
* @param namspacePrefix
* namespace prefix
* @param namespaceUri
* namespace URI
*/
protected void putNamespace(String namspacePrefix, String namespaceUri) {
namespaces.put(namspacePrefix, namespaceUri);
}
/**
* Remove a namespace in a map.
*
* @param namspacePrefix
* namespace prefix
*/
protected void removeNamespace(String namspacePrefix) {
namespaces.remove(namspacePrefix);
}
/**
* Get the name of the data store for a feature type. This is used to construct the name of the
* feature type directory as well as the name of the data store.
*
* @param namespacePrefix
* namespace prefix of the WFS feature type
* @param typeName
* local name of the WFS feature type
* @return name of the data store for the feature type
*/
protected static String getDataStoreName(String namespacePrefix, String typeName) {
return namespacePrefix + "_" + typeName;
}
/**
* Return the featureTypes directory under which individual feature type folders are stored.
*
* @return the featureTypes directory
*/
protected File getFeatureTypesBaseDir() {
return featureTypesBaseDir;
}
/**
* Get the file for the directory that contains the mapping and property files.
*
* @param namespacePrefix
* namespace prefix of the WFS feature type
* @param typeName
* local name of the WFS feature type
* @return directory that contains the mapping and property files
*/
private static File getFeatureTypeDir(File featureTypesBaseDir, String namespacePrefix,
String typeName) {
return new File(featureTypesBaseDir, getDataStoreName(namespacePrefix, typeName));
}
/**
* Copy the mapping and property files to the feature type directory.
*
* @param namespacePrefix
* namespace prefix of the WFS feature type
* @param typeName
* local name of the WFS feature type
* @param mappingFileName
* name of the mapping file for this feature type
* @param supportFileNames
* names of the support files, such as properties files, for this feature type
*/
private void copyMappingAndSupportFiles(String namespacePrefix, String typeName,
String mappingFileName, String... supportFileNames) {
onlineTestId = System.getProperty("testDatabase");
if (onlineTestId != null) {
onlineTestId = onlineTestId.toLowerCase().trim();
// special handling for running app-schema-test with online mode
try {
// new content with modified dataStore "parameters" tag
String newContent = modifyOnlineMappingFileContent(mappingFileName);
copy(newContent, "featureTypes/"
+ getDataStoreName(namespacePrefix, typeName)
+ "/"
+ mappingFileName.substring(mappingFileName.lastIndexOf("/") + 1,
mappingFileName.length()));
for (String propertyFileName : supportFileNames) {
if (propertyFileName.endsWith(".xml")) {
// also update the datastore "parameters" for supporting mapping files
newContent = modifyOnlineMappingFileContent(propertyFileName);
copy(newContent, "featureTypes/"
+ getDataStoreName(namespacePrefix, typeName)
+ "/"
+ propertyFileName.substring(propertyFileName.lastIndexOf("/") + 1,
propertyFileName.length()));
} else {
copyFileToFeatureTypeDir(namespacePrefix, typeName, propertyFileName);
if (propertyFileName.endsWith(".properties")) {
propertiesFiles.put(propertyFileName, getFeatureTypeDir(
featureTypesBaseDir, namespacePrefix, typeName));
}
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
copyFileToFeatureTypeDir(namespacePrefix, typeName, mappingFileName);
for (String propertyFileName : supportFileNames) {
copyFileToFeatureTypeDir(namespacePrefix, typeName, propertyFileName);
}
}
}
/**
* Modify the mapping file stream that is to be copied to the target directory. This is so the
* mapping file copy has the right datastore parameters to use the test database.
*
* @param mappingFileName
* Mapping file to be copied
* @return Modified content string
* @throws IOException
*/
private String modifyOnlineMappingFileContent(String mappingFileName) throws IOException {
InputStream is = AppSchemaDataAccessTest.class.getResourceAsStream(TEST_DATA
+ mappingFileName);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
StringBuffer content = new StringBuffer();
boolean parametersStartFound = false;
boolean parametersEndFound = false;
boolean isOracle = onlineTestId.equals("oracle");
for (String line = br.readLine(); line != null; line = br.readLine()) {
if (!parametersStartFound || (parametersStartFound && parametersEndFound)) {
// before <parameters> or after </parameters>
if (!parametersStartFound) {
// look for start tag
if (line.trim().equals("<parameters>")) {
parametersStartFound = true;
// copy <parameters> with new db params
if (isOracle) {
content.append(AppSchemaTestOracleSetup.DB_PARAMS);
} else {
content.append(AppSchemaTestPostgisSetup.DB_PARAMS);
}
} else {
// copy content
content.append(line);
}
} else if (line.trim().startsWith("<sourceType>")) {
// make everything upper case due to OracleDialect not wrapping them in quotes
line = line.trim();
String sourceTypeTag = "<sourceType>";
content.append(sourceTypeTag);
String tableName = line.substring(line.indexOf(sourceTypeTag)
+ sourceTypeTag.length(), line.indexOf("</sourceType>"));
content.append(tableName.toUpperCase());
content.append("</sourceType>");
content.append("\n");
} else {
content.append(line);
}
content.append("\n");
} else {
// else skip <parameters> content and do nothing
// look for end tag
if (line.trim().equals("</parameters>")) {
parametersEndFound = true;
}
}
}
return content.toString();
}
}