/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wps.gs;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogBuilder;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.ProjectionPolicy;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.wps.WPSException;
import org.geotools.data.DataStore;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.Transaction;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.process.ProcessException;
import org.geotools.process.factory.DescribeParameter;
import org.geotools.process.factory.DescribeProcess;
import org.geotools.process.factory.DescribeResult;
import org.geotools.process.gs.GSProcess;
import org.geotools.referencing.CRS;
import org.geotools.util.logging.Logging;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
/**
* Imports a feature collection into the GeoServer catalog
*
* @author Andrea Aime - OpenGeo
*
*/
@DescribeProcess(title = "Catalog import", description = "Imports the provided feature collection into the catalog")
public class ImportProcess implements GSProcess {
static final Logger LOGGER = Logging.getLogger(ImportProcess.class);
private Catalog catalog;
public ImportProcess(Catalog catalog) {
this.catalog = catalog;
}
@DescribeResult(name = "layerName", description = "The qualified name of the created layer (workspace:name)")
public String execute(
@DescribeParameter(name = "features", description = "The features that will make up the new GeoServer layer") SimpleFeatureCollection features,
@DescribeParameter(name = "workspace", min = 0, description = "The target workspace (the default one will be used if omitted)") String workspace,
@DescribeParameter(name = "store", min = 0, description = "The target store (the workspace default one will be used if omitted)") String store,
@DescribeParameter(name = "name", min = 0, description = "The name of the layer to be created (if missing the name of the features contained in the collection will be used") String name,
@DescribeParameter(name = "srs", min = 0, description = "The target coordinate reference system (the feature collection one will be analyzed and used if possible)") CoordinateReferenceSystem srs,
@DescribeParameter(name = "srsHandling", min = 0, description = "The desired SRS handling, FORCE_DECLARED will be used if not specified") ProjectionPolicy srsHandling,
@DescribeParameter(name = "styleName", min = 0, description = "The name of the style to be used for the layer. If missing a default style will be chosen according to the type of geometries contained in the collection") String styleName)
throws ProcessException {
// first off, decide what is the target store
WorkspaceInfo ws;
if (workspace != null) {
ws = catalog.getWorkspaceByName(workspace);
if (ws == null) {
throw new ProcessException("Could not find workspace " + workspace);
}
} else {
ws = catalog.getDefaultWorkspace();
if (ws == null) {
throw new ProcessException(
"The catalog is empty, could not find a default workspace");
}
}
// ok, find the target store
DataStoreInfo storeInfo;
if (store != null) {
storeInfo = catalog.getDataStoreByName(ws.getName(), store);
if (storeInfo == null) {
throw new ProcessException("Could not find store " + store + " in workspace "
+ workspace);
// TODO: support store creation
}
} else {
storeInfo = catalog.getDefaultDataStore(ws);
if (storeInfo == null) {
throw new ProcessException("Could not find a default store in workspace "
+ ws.getName());
}
}
// check if the target layer and the target feature type are not
// already there (this is a half-assed attempt as we don't have
// an API telling us how the feature type name will be changed
// by DataStore.createSchema(...), but better than fully importing
// the data into the target store to find out we cannot create the layer...)
String tentativeTargetName = null;
if (name != null) {
tentativeTargetName = ws.getName() + ":" + name;
} else {
tentativeTargetName = ws.getName() + ":" + features.getSchema().getTypeName();
}
if (catalog.getLayer(tentativeTargetName) != null) {
throw new ProcessException("Target layer " + tentativeTargetName + " already exists");
}
// check the target style if any
StyleInfo targetStyle = null;
if (styleName != null) {
targetStyle = catalog.getStyleByName(styleName);
if (targetStyle == null) {
throw new ProcessException("Could not find style " + styleName);
}
}
// check the target crs
String targetSRSCode = null;
if (srs != null) {
try {
Integer code = CRS.lookupEpsgCode(srs, true);
if (code == null) {
throw new WPSException("Could not find a EPSG code for " + srs);
}
targetSRSCode = "EPSG:" + code;
} catch (Exception e) {
throw new ProcessException("Could not lookup the EPSG code for the provided srs", e);
}
} else {
// check we can extract a code from the original data
GeometryDescriptor gd = features.getSchema().getGeometryDescriptor();
if (gd == null) {
// data is geometryless, we need a fake SRS
targetSRSCode = "EPSG:4326";
srsHandling = ProjectionPolicy.FORCE_DECLARED;
} else {
CoordinateReferenceSystem nativeCrs = gd.getCoordinateReferenceSystem();
if (nativeCrs == null) {
throw new ProcessException("The original data has no native CRS, "
+ "you need to specify the srs parameter");
} else {
try {
Integer code = CRS.lookupEpsgCode(nativeCrs, true);
if (code == null) {
throw new ProcessException("Could not find an EPSG code for data "
+ "native spatial reference system: " + nativeCrs);
} else {
targetSRSCode = "EPSG:" + code;
}
} catch (Exception e) {
throw new ProcessException("Failed to loookup an official EPSG code for "
+ "the source data native " + "spatial reference system", e);
}
}
}
}
// import the data into the target store
SimpleFeatureType targetType;
try {
targetType = importDataIntoStore(features, name, storeInfo);
} catch (IOException e) {
throw new ProcessException("Failed to import data into the target store", e);
}
// now import the newly created layer into GeoServer
try {
CatalogBuilder cb = new CatalogBuilder(catalog);
cb.setStore(storeInfo);
// build the typeInfo and set CRS if necessary
FeatureTypeInfo typeInfo = cb.buildFeatureType(targetType.getName());
if (targetSRSCode != null) {
typeInfo.setSRS(targetSRSCode);
}
if (srsHandling != null) {
typeInfo.setProjectionPolicy(srsHandling);
}
// compute the bounds
cb.setupBounds(typeInfo);
// build the layer and set a style
LayerInfo layerInfo = cb.buildLayer(typeInfo);
if (targetStyle != null) {
layerInfo.setDefaultStyle(targetStyle);
}
catalog.add(typeInfo);
catalog.add(layerInfo);
return typeInfo.getPrefixedName();
} catch (Exception e) {
throw new ProcessException(
"Failed to complete the import inside the GeoServer catalog", e);
}
}
private SimpleFeatureType importDataIntoStore(SimpleFeatureCollection features, String name,
DataStoreInfo storeInfo) throws IOException, ProcessException {
SimpleFeatureType targetType;
// grab the data store
DataStore ds = (DataStore) storeInfo.getDataStore(null);
// decide on the target ft name
SimpleFeatureType sourceType = features.getSchema();
if (name != null) {
SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
tb.init(sourceType);
tb.setName(name);
sourceType = tb.buildFeatureType();
}
// create the schema
ds.createSchema(sourceType);
// try to get the target feature type (might have slightly different
// name and structure)
targetType = ds.getSchema(sourceType.getTypeName());
if (targetType == null) {
// ouch, the name was changed... we can only guess now...
// try with the typical Oracle mangling
targetType = ds.getSchema(sourceType.getTypeName().toUpperCase());
}
if (targetType == null) {
throw new WPSException(
"The target schema was created, but with a name "
+ "that we cannot relate to the one we provided the data store. Cannot proceeed further");
} else {
// check the layer is not already there
String newLayerName = storeInfo.getWorkspace().getName() + ":"
+ targetType.getTypeName();
LayerInfo layer = catalog.getLayerByName(newLayerName);
// todo: we should not really reach here and know beforehand what the targetType
// name is, but if we do we should at least get a way to drop it
if (layer != null) {
throw new ProcessException("Target layer " + newLayerName
+ " already exists in the catalog");
}
}
// try to establish a mapping with old and new attributes. This is again
// just guesswork until we have a geotools api that will give us the
// exact mapping to be performed
Map<String, String> mapping = buildAttributeMapping(sourceType, targetType);
// start a transaction and fill the target with the input features
Transaction t = new DefaultTransaction();
SimpleFeatureStore fstore = (SimpleFeatureStore) ds.getFeatureSource(targetType
.getTypeName());
fstore.setTransaction(t);
SimpleFeatureIterator fi = features.features();
SimpleFeatureBuilder fb = new SimpleFeatureBuilder(targetType);
while (fi.hasNext()) {
SimpleFeature source = fi.next();
fb.reset();
for (String sname : mapping.keySet()) {
fb.set(mapping.get(sname), source.getAttribute(sname));
}
SimpleFeature target = fb.buildFeature(null);
fstore.addFeatures(DataUtilities.collection(target));
}
t.commit();
t.close();
return targetType;
}
/**
* Applies a set of heuristics to find which target attribute corresponds to a certain input
* attribute
*
* @param sourceType
* @param targetType
* @return
*/
Map<String, String> buildAttributeMapping(SimpleFeatureType sourceType,
SimpleFeatureType targetType) {
// look for the typical manglings. For example, if the target is a
// shapefile store it will move the geometry and name it the_geom
// collect the source names
Set<String> sourceNames = new HashSet<String>();
for (AttributeDescriptor sd : sourceType.getAttributeDescriptors()) {
sourceNames.add(sd.getLocalName());
}
// first check if we have been kissed by sheer luck and the names are
// the same
Map<String, String> result = new HashMap<String, String>();
for (String name : sourceNames) {
if (targetType.getDescriptor(name) != null) {
result.put(name, name);
}
}
sourceNames.removeAll(result.keySet());
// then check for simple case difference (Oracle case)
for (String name : sourceNames) {
for (AttributeDescriptor td : targetType.getAttributeDescriptors()) {
if (td.getLocalName().equalsIgnoreCase(name)) {
result.put(name, td.getLocalName());
break;
}
}
}
sourceNames.removeAll(result.keySet());
// then check attribute names being cut (another Oracle case)
for (String name : sourceNames) {
String loName = name.toLowerCase();
for (AttributeDescriptor td : targetType.getAttributeDescriptors()) {
String tdName = td.getLocalName().toLowerCase();
if (loName.startsWith(tdName)) {
result.put(name, td.getLocalName());
break;
}
}
}
sourceNames.removeAll(result.keySet());
// consider the shapefile geometry descriptor mangling
if (targetType.getGeometryDescriptor() != null
&& "the_geom".equals(targetType.getGeometryDescriptor().getLocalName())
&& !"the_geom".equalsIgnoreCase(sourceType.getGeometryDescriptor().getLocalName())) {
result.put(sourceType.getGeometryDescriptor().getLocalName(), "the_geom");
}
// and finally we return with as much as we can match
if (!sourceNames.isEmpty()) {
LOGGER.warning("Could not match the following attributes " + sourceNames
+ " to the target feature type ones: " + targetType);
}
return result;
}
}