/* 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.wfs.response;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.zip.ZipOutputStream;
import javax.xml.namespace.QName;
import net.opengis.wfs.FeatureCollectionType;
import net.opengis.wfs.GetFeatureType;
import net.opengis.wfs.QueryType;
import org.geoserver.config.GeoServer;
import org.geoserver.data.util.IOUtils;
import org.geoserver.ows.util.OwsUtils;
import org.geoserver.platform.Operation;
import org.geoserver.platform.ServiceException;
import org.geoserver.wfs.WFSException;
import org.geoserver.wfs.WFSGetFeatureOutputFormat;
import org.geoserver.wfs.request.FeatureCollectionResponse;
import org.geotools.data.DataStore;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.data.store.EmptyFeatureCollection;
import org.geotools.feature.FeatureTypes;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.gml.producer.FeatureTransformer;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.feature.type.GeometryType;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
public class Ogr2OgrOutputFormat extends WFSGetFeatureOutputFormat {
/**
* The types of geometries a shapefile can handle
*/
private static final Set<Class> SHAPEFILE_GEOM_TYPES = new HashSet<Class>() {
{
add(Point.class);
add(LineString.class);
add(LinearRing.class);
add(Polygon.class);
add(MultiPoint.class);
add(MultiLineString.class);
add(MultiPolygon.class);
}
};
/**
* The fs path to ogr2ogr. If null, we'll assume ogr2ogr is in the PATH and
* that we can execute it just by running ogr2ogr
*/
String ogrPath = null;
/**
* The full path to ogr2ogr
*/
String ogrExecutable = "ogr2ogr";
/**
* The GDAL_DATA folder
*/
String gdalData = null;
/**
* The output formats we can generate using ogr2ogr. Using a concurrent
* one so that it can be reconfigured while the output format is working
*/
static Map<String, OgrFormat> formats = new ConcurrentHashMap<String, OgrFormat>();
public Ogr2OgrOutputFormat(GeoServer gs) {
// initialize with the key set of formats, so that it will change as
// we register new formats
super(gs, formats.keySet());
}
/**
* Returns the ogr2ogr executable full path
*
* @return
*/
public String getOgrExecutable() {
return ogrExecutable;
}
/**
* Sets the ogr2ogr executable full path. The default value is simply
* "ogr2ogr", which will work if ogr2ogr is in the path
*
* @param ogrExecutable
*/
public void setOgrExecutable(String ogrExecutable) {
this.ogrExecutable = ogrExecutable;
}
/**
* Returns the location of the gdal data folder (required to set the output srs)
* @return
*/
public String getGdalData() {
return gdalData;
}
/**
* Sets the location of the gdal data folder (requierd to set the output srs)
* @param gdalData
*/
public void setGdalData(String gdalData) {
this.gdalData = gdalData;
}
/**
* @see WFSGetFeatureOutputFormat#getMimeType(Object, Operation)
*/
public String getMimeType(Object value, Operation operation) throws ServiceException {
GetFeatureType request = (GetFeatureType) OwsUtils.parameter(operation.getParameters(),
GetFeatureType.class);
OgrFormat format = formats.get(request.getOutputFormat());
if (format == null) {
throw new WFSException("Unknown output format " + request.getOutputFormat());
} else if (format.singleFile && request.getQuery().size() <= 1) {
if(format.mimeType != null) {
return format.mimeType;
} else {
// use a default binary blob
return "application/octet-stream";
}
} else {
return "application/zip";
}
}
@Override
public boolean canHandle(Operation operation) {
// we can't handle anything if the ogr2ogr configuration failed
if(formats.size() == 0) {
return false;
} else {
return super.canHandle(operation);
}
}
@Override
public String getPreferredDisposition(Object value, Operation operation) {
return DISPOSITION_ATTACH;
}
@Override
public String getAttachmentFileName(Object value, Operation operation) {
GetFeatureType request = (GetFeatureType) OwsUtils.parameter(operation.getParameters(),
GetFeatureType.class);
OgrFormat format = formats.get(request.getOutputFormat());
if (format == null) {
throw new WFSException("Unknown output format " + request.getOutputFormat());
} else if (!format.singleFile || request.getQuery().size() > 1) {
String outputFileName = ((QName) ((QueryType) request.getQuery().get(0)).getTypeName()
.get(0)).getLocalPart();
return outputFileName + ".zip";
} else {
return null;
}
}
/**
* Adds a ogr format among the supported ones
*
* @param parameters
*/
public void addFormat(OgrFormat parameters) {
formats.put(parameters.formatName, parameters);
}
/**
* Programmatically removes all formats
*
* @param parameters
*/
public void clearFormats() {
formats.clear();
}
/**
* Writes out the data to an OGR known format (GML/shapefile) to disk and
* then ogr2ogr each generated file into the destination format. Finally,
* zips up all the resulting files.
*/
@Override
protected void write(FeatureCollectionResponse featureCollection, OutputStream output,
Operation getFeature) throws IOException ,ServiceException {
// figure out which output format we're going to generate
GetFeatureType gft = (GetFeatureType) getFeature.getParameters()[0];
OgrFormat format = formats.get(gft.getOutputFormat());
if (format == null)
throw new WFSException("Unknown output format " + gft.getOutputFormat());
// create the first temp directory, used for dumping gs generated
// content
File tempGS = org.geoserver.data.util.IOUtils.createTempDirectory("ogrtmpin");
File tempOGR = org.geoserver.data.util.IOUtils.createTempDirectory("ogrtmpout");
// build the ogr wrapper used to run the ogr2ogr commands
OGRWrapper wrapper = new OGRWrapper(ogrExecutable, gdalData);
// actually export each feature collection
try {
Iterator outputFeatureCollections = featureCollection.getFeature().iterator();
SimpleFeatureCollection curCollection;
File outputFile = null;
while (outputFeatureCollections.hasNext()) {
curCollection = (SimpleFeatureCollection) outputFeatureCollections
.next();
// write out the gml
File intermediate = writeToDisk(tempGS, curCollection);
// convert with ogr2ogr
final SimpleFeatureType schema = curCollection.getSchema();
final CoordinateReferenceSystem crs = schema.getCoordinateReferenceSystem();
outputFile = wrapper.convert(intermediate, tempOGR, schema.getTypeName(), format, crs);
// wipe out the input dir contents
IOUtils.emptyDirectory(tempGS);
}
// was is a single file output?
if(format.singleFile && featureCollection.getFeature().size() == 1) {
FileInputStream fis = null;
try {
fis = new FileInputStream(outputFile);
org.apache.commons.io.IOUtils.copy(fis, output);
} finally {
if(fis != null) {
fis.close();
}
}
} else {
// scan the output directory and zip it all
ZipOutputStream zipOut = new ZipOutputStream(output);
IOUtils.zipDirectory(tempOGR, zipOut, null);
zipOut.finish();
}
// delete the input and output directories
IOUtils.delete(tempGS);
IOUtils.delete(tempOGR);
} catch (Exception e) {
throw new ServiceException("Exception occurred during output generation", e);
}
}
/**
* Writes to disk using shapefile if the feature type allows for it, GML otherwise
* @param tempDir
* @param curCollection
* @return
*/
private File writeToDisk(File tempDir,
SimpleFeatureCollection curCollection) throws Exception {
// ogr2ogr cannot handle empty gml collections, but it can handle empty
// shapefiles
final SimpleFeatureType originalSchema = curCollection.getSchema();
if(curCollection.isEmpty()) {
if(isShapefileCompatible(originalSchema)) {
return writeShapefile(tempDir, curCollection);
} else {
SimpleFeatureType simplifiedShema = buildShapefileCompatible(originalSchema);
return writeShapefile(tempDir, new EmptyFeatureCollection(simplifiedShema));
}
}
// create the temp file for this output
File outFile = new File(tempDir, originalSchema.getTypeName() + ".gml");
// write out
OutputStream os = null;
try {
os = new FileOutputStream(outFile);
// let's invoke the transformer
FeatureTransformer ft = new FeatureTransformer();
ft.setNumDecimals(16);
ft.getFeatureNamespaces().declarePrefix("gs",
originalSchema.getName().getNamespaceURI());
ft.transform(curCollection, os);
} finally {
os.close();
}
return outFile;
}
private SimpleFeatureType buildShapefileCompatible(SimpleFeatureType originalSchema) {
SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
tb.setName(originalSchema.getName());
// add the fake geometry
tb.add("the_geom", Point.class, originalSchema.getCoordinateReferenceSystem());
// preserve all othr attributes
for (AttributeDescriptor at : originalSchema.getAttributeDescriptors()) {
if(!(at instanceof GeometryDescriptor)) {
tb.add(at);
}
}
return tb.buildFeatureType();
}
/**
* Returns true if the schema has just one geometry and the geom type is known
* @param schema
* @return
*/
private boolean isShapefileCompatible(SimpleFeatureType schema) {
GeometryType gt = null;
for (AttributeDescriptor at : schema.getAttributeDescriptors()) {
if(at instanceof GeometryDescriptor) {
if(gt == null)
gt = ((GeometryDescriptor) at).getType();
else
// more than one geometry
return false;
}
}
return gt != null && SHAPEFILE_GEOM_TYPES.contains(gt.getBinding());
}
private File writeShapefile(File tempDir,
SimpleFeatureCollection collection) {
SimpleFeatureType schema = collection.getSchema();
SimpleFeatureStore fstore = null;
DataStore dstore = null;
File file = null;
try {
file = new File(tempDir, schema.getTypeName() + ".shp");
dstore = new ShapefileDataStore(file.toURL());
dstore.createSchema(schema);
fstore = (SimpleFeatureStore) dstore.getFeatureSource(schema.getTypeName());
fstore.addFeatures(collection);
} catch (IOException ioe) {
LOGGER.log(Level.WARNING,
"Error while writing featuretype '" + schema.getTypeName() + "' to shapefile.", ioe);
throw new ServiceException(ioe);
} finally {
if(dstore != null) {
dstore.dispose();
}
}
return file;
}
}