/* (c) 2014 - 2015 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.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.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.zip.ZipOutputStream; import org.geoserver.config.GeoServer; import org.geoserver.data.util.IOUtils; import org.geoserver.ogr.core.Format; import org.geoserver.ogr.core.FormatConverter; import org.geoserver.ogr.core.OutputType; import org.geoserver.ogr.core.ToolWrapper; import org.geoserver.ogr.core.ToolWrapperFactory; 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.geoserver.wfs.request.GetFeatureRequest; import org.geoserver.wfs.request.Query; 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.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 implements FormatConverter { /** * 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); } }; /** * Factory to create the ogr2ogr wrapper. */ ToolWrapperFactory ogrWrapperFactory; /** * 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 environment variables to set before invoking ogr2ogr */ Map<String, String> environment = 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, Format> formats = new ConcurrentHashMap<String, Format>(); public Ogr2OgrOutputFormat(GeoServer gs, ToolWrapperFactory wrapperFactory) { // initialize with the key set of formats, so that it will change as // we register new formats super(gs, formats.keySet()); this.ogrWrapperFactory = wrapperFactory; this.environment = new HashMap<String, String>(); } /** * Returns the ogr2ogr executable full path * * */ @Override public String getExecutable() { 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 */ @Override public void setExecutable(String ogrExecutable) { this.ogrExecutable = ogrExecutable; } /** * Returns the environment variables that are set prior to invoking ogr2ogr * * */ @Override public Map<String, String> getEnvironment() { return environment; } /** * Provides the environment variables that are set prior to invoking ogr2ogr (notably the GDAL_DATA variable, specifying the location of * GDAL's data directory). * * @param environment */ @Override public void setEnvironment(Map<String, String> environment) { if (environment != null) { this.environment.clear(); this.environment.putAll(environment); } } /** * @see WFSGetFeatureOutputFormat#getMimeType(Object, Operation) */ public String getMimeType(Object value, Operation operation) throws ServiceException { GetFeatureRequest request = GetFeatureRequest.adapt(operation.getParameters()[0]); String outputFormat = request.getOutputFormat(); String mimeType = ""; Format format = formats.get(outputFormat); if (format == null) { throw new WFSException("Unknown output format " + outputFormat); } else if (format.isSingleFile() && request.getQueries().size() <= 1) { if (format.getMimeType() != null) { mimeType = format.getMimeType(); } else { // use a default binary blob mimeType = "application/octet-stream"; } } else { mimeType = "application/zip"; } return mimeType; } @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) { GetFeatureRequest request = GetFeatureRequest.adapt(operation.getParameters()[0]); String outputFormat = request.getOutputFormat(); Format format = formats.get(outputFormat); List<Query> queries = request.getQueries(); if (format == null) { throw new WFSException("Unknown output format " + outputFormat); } else if(format.getType() == OutputType.BINARY || queries.size() > 1) { return DISPOSITION_ATTACH; } else { return DISPOSITION_INLINE; } } @Override public String getAttachmentFileName(Object value, Operation operation) { GetFeatureRequest request = GetFeatureRequest.adapt(operation.getParameters()[0]); String outputFormat = request.getOutputFormat(); Format format = formats.get(outputFormat); List<Query> queries = request.getQueries(); if (format == null) { throw new WFSException("Unknown output format " + outputFormat); } else { String outputFileName = queries.get(0).getTypeNames().get(0).getLocalPart(); if(!format.isSingleFile() || queries.size() > 1) { return outputFileName + ".zip"; } else { return outputFileName + format.getFileExtension(); } } } /** * Adds a ogr format among the supported ones * * @param parameters */ @Override public void addFormat(Format parameters) { formats.put(parameters.getGeoserverFormat(), parameters); } /** * Get a list of supported ogr formats * * */ @Override public List<Format> getFormats() { return new ArrayList<Format>(formats.values()); } @Override public void clearFormats() { formats.clear(); } @Override public void replaceFormats(List<Format> newFormats) { clearFormats(); for (Format newFormat: newFormats) { addFormat(newFormat); } } /** * 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 GetFeatureRequest request = GetFeatureRequest.adapt(getFeature.getParameters()[0]); String outputFormat = request.getOutputFormat(); Format format = formats.get(outputFormat); if (format == null) throw new WFSException("Unknown output format " + outputFormat); // 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 ToolWrapper wrapper = new OGRWrapper(ogrExecutable, environment); // 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.isSingleFile() && 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 = null; try { zipOut = new ZipOutputStream(output); IOUtils.zipDirectory(tempOGR, zipOut, null); zipOut.finish(); } finally { org.apache.commons.io.IOUtils.closeQuietly(zipOut); } } // 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 * */ 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 * */ 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; } @Override public List<String> getCapabilitiesElementNames() { return getAllCapabilitiesElementNames(); } }