/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved. * This code is licensed under the GPL 2.0 license, availible at the root * application directory. */ package org.geoserver.wfs.response; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Serializable; import java.io.StringWriter; import java.net.MalformedURLException; import java.nio.charset.Charset; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.SimpleTimeZone; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.ZipOutputStream; import javax.servlet.http.HttpServletRequest; import javax.xml.namespace.QName; import net.opengis.wfs.FeatureCollectionType; import net.opengis.wfs.GetFeatureType; import net.opengis.wfs.QueryType; import org.apache.commons.io.FileUtils; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogBuilder; import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.catalog.MetadataMap; import org.geoserver.config.GeoServer; import org.geoserver.data.util.IOUtils; import org.geoserver.feature.RetypingFeatureCollection; import org.geoserver.ows.Dispatcher; import org.geoserver.ows.Request; import org.geoserver.ows.util.OwsUtils; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.platform.Operation; import org.geoserver.platform.ServiceException; import org.geoserver.template.GeoServerTemplateLoader; import org.geoserver.wfs.WFSException; import org.geoserver.wfs.WFSGetFeatureOutputFormat; import org.geoserver.wfs.WFSInfo; import org.geoserver.wfs.request.FeatureCollectionResponse; import org.geotools.data.DataStore; import org.geotools.data.DataUtilities; import org.geotools.data.FeatureWriter; import org.geotools.data.Transaction; import org.geotools.data.shapefile.ShapefileDataStore; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.data.simple.SimpleFeatureStore; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.geometry.jts.Geometries; import org.geotools.jdbc.JDBCDataStore; import org.geotools.referencing.CRS; import org.geotools.util.logging.Logging; import org.geotools.wfs.v1_1.WFS; import org.geotools.wfs.v1_1.WFSConfiguration; import org.geotools.xml.Encoder; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.AttributeType; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.feature.type.GeometryType; import org.opengis.feature.type.Name; import org.opengis.referencing.FactoryException; import org.opengis.referencing.ReferenceIdentifier; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.util.InternationalString; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.LineString; 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; import freemarker.template.Configuration; import freemarker.template.Template; import org.apache.commons.dbcp.BasicDataSource; import org.geotools.jdbc.JDBCDataStore; import org.geotools.jdbc.JDBCDataStoreFactory; import org.geotools.jdbc.SQLDialect; import org.sqlite.SQLiteConfig; import org.spatialite.libs.MultiLibs; import org.spatialite.libs.MultiLibs; //import org.spatialite.libs.MultiLibs; /** * * WFS output format for a GetFeature operation in which the outputFormat is "spatialite". * The reference documentation for this format can be found in this link: * @link:http://www.gaia-gis.it/spatialite/docs.html. * * Based on CSVOutputFormat.java and ShapeZipOutputFormat.java from geoserver 2.2.x * * @author Pablo Velazquez, Geotekne, info@geotekne.com * @author Jose Macchi, Geotekne, jmacchi@geotekne.com * */ public class SpatiaLiteOutputFormat extends WFSGetFeatureOutputFormat { //A Hashtable to convert the native data types to data types supported by SQLite private Hashtable<String, String> dataTypes = new Hashtable(); private Hashtable<Class<? extends Geometry>, String> geomTypes = new Hashtable(); private String driverClassName = "org.sqlite.JDBC"; public SpatiaLiteOutputFormat(GeoServer gs) { super(gs,"SpatiaLite"); //Initializing the dataTypes Hashtable dataTypes.put("STRING","TEXT"); dataTypes.put("INT","INTEGER"); dataTypes.put("LONG","INTEGER"); dataTypes.put("BOOL","INTEGER"); dataTypes.put("DOUBLE","REAL"); dataTypes.put("FLOAT","REAL"); //Initializing the geomTypes Hashtable geomTypes.put(Geometries.POINT.getBinding(),"POINT"); geomTypes.put(Geometries.LINESTRING.getBinding(),"LINESTRING"); geomTypes.put(Geometries.POLYGON.getBinding(),"POLYGON"); geomTypes.put(Geometries.MULTIPOINT.getBinding(),"MULTIPOINT"); geomTypes.put(Geometries.MULTILINESTRING.getBinding(),"MULTILINESTRING"); geomTypes.put(Geometries.MULTIPOLYGON.getBinding(),"MULTIPOLYGON"); geomTypes.put(Geometries.GEOMETRY.getBinding(), "GEOMETRY"); geomTypes.put(Geometries.GEOMETRYCOLLECTION.getBinding(), "GEOMETRYCOLLECTION"); } /** * @return "application/x-sqlite3"; */ @Override public String getMimeType(Object value, Operation operation) throws ServiceException { return "application/x-sqlite3"; } @Override protected boolean canHandleInternal(Operation operation) { //any additional checks that need to be performed to // determine when the output format should be "engaged" // should go here return super.canHandleInternal(operation); } @Override protected void write(FeatureCollectionResponse featureCollection, OutputStream output, Operation getFeature) throws IOException, ServiceException { List<SimpleFeatureCollection> collections = new ArrayList<SimpleFeatureCollection>(); collections.addAll((Collection<? extends SimpleFeatureCollection>) featureCollection.getFeature()); Charset charset = Charset.forName("UTF-8"); write(collections, charset, output, (GetFeatureType) getFeature.getParameters()[0]); } protected void write(List<SimpleFeatureCollection> collections, Charset charset, OutputStream output, GetFeatureType request) throws IOException, ServiceException { Connection conn = null; /** * Get the necessary JDBC object. */ try { Class.forName(this.driverClassName); } catch (ClassNotFoundException e) { System.out.println(e); } /** * base location to temporally store spatialite database files */ File tempDir = File.createTempFile("spatialitemp", ".sqlite"); /** * enables load extension */ SQLiteConfig config = new SQLiteConfig(); config.enableLoadExtension(true); /** * the Url for the temporally sqlite file */ String JDBCFileUrl = tempDir.getAbsolutePath(); try { //create a connection to database conn = DriverManager.getConnection("jdbc:sqlite:"+JDBCFileUrl,config.toProperties()); Statement stmt = conn.createStatement(); stmt.setQueryTimeout(30); /** * A string to store the statements to run to create the Spatialite DataBase */ String sql = null; String spatialiteLibraryUrl = MultiLibs.loadExtension(); if (spatialiteLibraryUrl != null) { sql = "SELECT load_extension('"+spatialiteLibraryUrl+"')"; stmt.execute(sql); sql = "SELECT InitSpatialMetaData();"; stmt.execute(sql); /** * A string to store the names of the columns that will be used to populate the table */ String column_names = null; //We might get multiple feature collections in our response so we need to //write out multiple tables, one for each query response. conn.setAutoCommit(false); for (SimpleFeatureCollection fc : collections) { //get the current feature SimpleFeatureType ft = fc.getSchema(); //To check if the current feature has a geometry. String the_geom = null; if(ft.getGeometryDescriptor() != null) { the_geom = ft.getGeometryDescriptor().getLocalName(); } //Get the table name for the current feature String tbl_name = ft.getName().getLocalPart(); /** * Create the table for the current feature as follows: * - first get the statement for create the table * - execute the statement * - second get the statement for add the geometry (if has one) * - execute the statement */ //Initialize the "create table" query. column_names = ""; int column_cnt = 0; sql = "CREATE TABLE "+tbl_name; sql += " ( PK_UID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT"; //Get the columns names for the table tbl_name for ( int i = 0; i < ft.getAttributeCount(); i++ ) { AttributeDescriptor ad = ft.getDescriptor( i ); if (ad.getLocalName() != the_geom){ sql += ", "+prepareColumnHeader( ad ); column_names += ad.getLocalName(); column_cnt++; if ( i < ft.getAttributeCount()-1){ column_names += ", "; } } } sql += ");"; //Quick Fix in Column_names if (column_names.endsWith(", ")) column_names = column_names.substring(0,column_names.length()-2); // Finish creating the table stmt.execute(sql); conn.commit(); int srid = 0; //If the table : "tbl_name" has a geometry, then i write the sql to add the geometry if (the_geom != null){ sql = "SELECT AddGeometryColumn('"+tbl_name+"', "; //get the geometry type sql += "'"+the_geom+"', "; //get the SRID. srid = getSpatialSRID(ft.getCoordinateReferenceSystem()); sql += srid+", "; //get the Geometry type. String geom_type = getSpatialGeometryType(ft); if (geom_type == null){ throw new WFSException("Error while adding the geometry column in table " + tbl_name+ ", unrecognized geometry type"); } sql += "'"+geom_type+"', "; //get Dimensions, we only works whit 2 dimensions. String dimension = "XY"; sql += "'" + dimension + "'"; sql += " );"; } //finish creating the geometry column. stmt.execute(sql); conn.commit(); /** * Populates the table for the current feature as follows: * For each row * - first: configure the statement with the appropriates fields. * - second: add to the statement the field the_geom if has a geometry. * - third: configure the statement with the appropriates values. * (if has a geometry i add that value) * - execute the statement * Finally commit. */ //Start populating the table: tbl_name. SimpleFeatureIterator i = fc.features(); try { while( i.hasNext() ) { SimpleFeature row = i.next(); sql = "INSERT INTO "+tbl_name+" ("+column_names; //if has a geometry, i add the field the_geom. if (the_geom != null) if (column_cnt > 0 ) { sql += ", "+the_geom+" ) "; } else { sql += the_geom+") "; } else{ sql += ") "; } //I store the default geometry value, so i can omit it and add at the end. Object geom_data = row.getDefaultGeometry(); sql += "VALUES ("; for ( int j = 0; j < row.getAttributeCount(); j++ ) { Object rowAtt = row.getAttribute( j ); if (!rowAtt.equals(geom_data)){ if ( rowAtt != null ){ //We just transform all content to String. sql += "'"+rowAtt.toString()+"'"; if ( j < row.getAttributeCount()-1) sql += ", "; } } } if (sql.endsWith(", ")) sql = sql.substring(0, sql.length()-2); //Finally if has geometry, insert the geometry data. if (the_geom != null){ if (column_cnt > 0 ){ sql += ", "; } sql += "GeomFromText('"+prepareGeom(geom_data.toString())+"', "+srid+")"; } sql += ");"; stmt.execute(sql); } conn.commit(); } finally { fc.close( i ); } } conn.close(); /** * Loads the temp dll in a file and delete it */ File spatialiteLibraryFile = new File(spatialiteLibraryUrl); spatialiteLibraryFile.delete(); } else { throw new WFSException("Error loading spatialite native library." + "platform or OS not supported yet"); } } catch (SQLException e) { System.out.println(e); } /** * A FileInputStream to read the tempDir in a byte array * so i can write this in the OutputStream output and flush it. */ FileInputStream JDBCIn = new FileInputStream(tempDir); int dataLengh = JDBCIn.available(); byte[] data = new byte[dataLengh]; JDBCIn.read(data); output.write(data); JDBCIn.close(); tempDir.delete(); } public String getCapabilitiesElementName() { return "SPATIALITE"; } @Override public String getPreferredDisposition(Object value, Operation operation) { return DISPOSITION_ATTACH; } @Override public String getAttachmentFileName(Object value, Operation operation) { SimpleFeatureCollection fc = (SimpleFeatureCollection) ((FeatureCollectionType) value).getFeature().get(0); GetFeatureType request = (GetFeatureType) OwsUtils.parameter(operation.getParameters(), GetFeatureType.class); String outputFileName = null; if (request != null) { Map<String, ?> formatOptions = request.getFormatOptions(); outputFileName = (String) formatOptions.get("FILENAME"); } if (outputFileName == null) { outputFileName = ((QName) ((QueryType) request.getQuery().get(0)).getTypeName().get(0)) .getLocalPart(); } return outputFileName + ".sqlite"; } //Returns the geometry type of a feature. //If the feature doesn't have a recognized geometry type this return a null value. private String getSpatialGeometryType(SimpleFeatureType featureType){ Class<?> geomType = featureType.getGeometryDescriptor().getType().getBinding(); return (String)geomTypes.get(geomType); } //Get the Current SRID of a Feature. //If the feature doesn't have a SRID, this return SRID = "-1" (default SpatiaLite SRID) private int getSpatialSRID(CoordinateReferenceSystem crs){ try { return CRS.lookupEpsgCode(crs, true); } catch (FactoryException e) { System.out.println(e.getMessage()); return -1; } } //Prepares the column headers whit the format: // "COLUMN_NAME" + "COLUMN_TYPE" private String prepareColumnHeader( AttributeDescriptor ad ){ //I split the binding, and get the last split, that represents the data type String[] split = ad.getType().getBinding().getName().split("\\."); String column_type = (String)dataTypes.get(split[split.length-1].toUpperCase()); return ad.getLocalName().toUpperCase()+" "+column_type; } /** * This method return a prepared MULTIPOINT geometry if is MULTIPOINT (We need do this * because MULTIPOINT Feature format is: MULTIPOINT ((x y),(x y),(x y)) and * MULTIPOINT Spatialite format is: MULTIPOINT (x y, x y, x y) * @param theGeom; * @return value; */ private String prepareGeom (String theGeom){ String value = theGeom; /*if (theGeom.contains("GEOMETRYCOLLECTION")) //Hago split; System.out.println("Not Yet"); else*/ if (theGeom.contains("MULTIPOINT")) { value = value.replaceAll("\\(\\(", "*"); value = value.replaceAll("\\)\\)", "#"); value = value.replaceAll("\\(", ""); value = value.replaceAll("\\)", ""); value = value.replaceAll("\\*", "("); value = value.replaceAll("\\#", ")"); } return value; } }