/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.data.postgis;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.geotools.data.jdbc.DefaultSQLBuilder;
import org.geotools.data.jdbc.JDBCDataStoreConfig;
import org.geotools.data.jdbc.fidmapper.FIDMapper;
import org.geotools.factory.Hints;
import org.geotools.filter.Filter;
import org.geotools.filter.SQLEncoder;
import org.geotools.filter.SQLEncoderException;
import org.geotools.filter.SQLEncoderPostgis;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
/**
* Builds sql for postgis.
*
* @author Chris Holmes
* @source $URL$
*/
public class PostgisSQLBuilder extends DefaultSQLBuilder {
/** If true, WKB format is used instead of WKT */
protected boolean WKBEnabled = false;
/** If true, ByteA function is used to transfer WKB data*/
protected boolean byteaEnabled = false;
/** If true, tables are qualified with a schema **/
protected boolean schemaEnabled = true;
/** the datastore **/
protected JDBCDataStoreConfig config;
/**
*
*/
public PostgisSQLBuilder(int srid, JDBCDataStoreConfig config) {
this((SQLEncoder) new SQLEncoderPostgis(srid),config);
}
/**
* Constructor with encoder. Use PostgisSQLBuilder(encoder, config, ft) if possible.
*
* @param encoder
*/
public PostgisSQLBuilder(SQLEncoder encoder, JDBCDataStoreConfig config) {
super(encoder);
this.config = config;
}
public PostgisSQLBuilder(SQLEncoder encoder, JDBCDataStoreConfig config, SimpleFeatureType ft) {
super(encoder);
this.config = config;
this.ft = ft;
encoder.setFeatureType( ft );
}
/**
* Overrides to support offset and maxFeatures
* @see DefaultSQLBuilder#buildSQLQuery(String, FIDMapper, AttributeDescriptor[], org.opengis.filter.Filter, SortBy[], Integer, Integer)
*/
@Override
public String buildSQLQuery(String typeName,
FIDMapper mapper,
AttributeDescriptor[] attrTypes,
org.opengis.filter.Filter filter,
SortBy[] sortBy,
Integer offset,
Integer limit) throws SQLEncoderException {
if(offset != null){
//we need to add the PK as sorting order regardless of the client asking for a specific order or not
//so we can ensure a consistent order if the client asks for ordering over an attribute other than the PK
List<SortBy> sortAtts = new ArrayList<SortBy>();
if(sortBy != null){
sortAtts.addAll(Arrays.asList(sortBy));
}
if(!(sortAtts.contains(SortBy.NATURAL_ORDER) || sortAtts.contains(SortBy.REVERSE_ORDER))){
//no natural order contained in the required list, append PK ordering...
sortAtts.add(SortBy.NATURAL_ORDER);
}
sortBy = sortAtts.toArray(new SortBy[sortAtts.size()]);
}
final String selectStatement = super.buildSQLQuery(typeName, mapper, attrTypes, filter, sortBy, offset, limit);
StringBuilder sb = new StringBuilder(selectStatement);
if(offset != null){
sb.append(" OFFSET ").append(offset);
}
if(limit != null){
sb.append(" LIMIT ").append(limit);
}
return sb.toString();
}
/**
* Overrides to support NATURAL_ORDER and REVERSE_ORDER
*/
@Override
protected void addOrderByPK(StringBuffer sql, FIDMapper mapper, SortOrder sortOrder)
throws SQLEncoderException {
if (mapper == null || mapper.getColumnCount() == 0) {
throw new SQLEncoderException(
"NATURAL_ORDER and REVERSE_ORDER is not supported without a primary key");
}
final String order = SortOrder.ASCENDING == sortOrder ? "ASC" : "DESC";
String colName;
final int columnCount = mapper.getColumnCount();
for (int idx = 0; idx < columnCount; idx++) {
colName = mapper.getColumnName(idx);
sql.append(colName).append(" ").append(order);
if (idx < columnCount - 1) {
sql.append(", ");
}
}
}
/**
* Produces the select information required.
* <p>
* The featureType, if known, is always requested.
* </p>
* <p>
* sql: <code>featureID (,attributeColumn)</code>
* </p>
* <p>
* We may need to provide AttributeReaders with a hook so they can request a wrapper function.
* </p>
*
* @param sql
* @param mapper
* @param attributes
*/
public void sqlColumns(StringBuffer sql, FIDMapper mapper,
AttributeDescriptor[] attributes) {
for (int i = 0; i < mapper.getColumnCount(); i++) {
sql.append("\""+mapper.getColumnName(i)+"\"");
// DJB: add quotes in. NOTE: if FID mapper isnt oid (ie. PK - Primary Key), you could be
// requesting PK columns multiple times
if ((attributes.length > 0) || (i < (mapper.getColumnCount() - 1))) {
sql.append(", ");
}
}
for (int i = 0; i < attributes.length; i++) {
AttributeDescriptor attribute = attributes[i];
if (attribute instanceof GeometryDescriptor) {
GeometryDescriptor geometryAttribute = (GeometryDescriptor) attribute;
CoordinateReferenceSystem crs = geometryAttribute.getCoordinateReferenceSystem();
final int D = isForce2D() ? 2 : -1;
if (WKBEnabled) {
if(byteaEnabled) {
columnGeometryByteaWKB( sql, geometryAttribute, D );
} else {
columnGeometryWKB( sql, geometryAttribute, D );
}
} else {
columnGeometry( sql, geometryAttribute, D );
}
} else {
columnAttribute(sql, attribute);
}
if (i < (attributes.length - 1)) {
sql.append(", ");
}
}
}
/** Used when WKB "ByteA" is enabled */
private void columnGeometryByteaWKB(StringBuffer sql,
GeometryDescriptor geometryAttribute, final int D) {
sql.append("encode(");
if( D == 2 ) {
sql.append("asBinary(");
} else {
sql.append("asEWKB(");
}
columnGeometry(sql, geometryAttribute.getLocalName(), D );
sql.append(",'XDR'),'base64')");
}
/** Used when plain WKB is enabled */
private void columnGeometryWKB(StringBuffer sql,
GeometryDescriptor geometryAttribute, final int D) {
if( D == 3 ){
sql.append("asEWKB(");
}
else {
sql.append("asBinary(");
}
columnGeometry(sql, geometryAttribute.getLocalName(), D );
sql.append(",'XDR')");
}
/** Used to request a text format. */
private void columnGeometry(StringBuffer sql,
GeometryDescriptor geometryAttribute, final int D) {
if( D == 3 && !isForce2D() ){
sql.append("asEWKT(");
}
else {
sql.append("asText(");
}
columnGeometry(sql, geometryAttribute.getLocalName(), D );
sql.append(")");
}
/**
* Used to wrap the correct function (force_3d or force3d) around
* the request for geometry data.
* <p>
* This method prevents the request of extra ordinates that will
* not be used.
*
* @see isForce2D
* @see Hints.FEATURE_2D
*
* @param sql
* @param geomName
* @param D
*/
private void columnGeometry(StringBuffer sql,String geomName, final int D) {
if (D == 2 || isForce2D() ){
sql.append("force_2d(\"" + geomName + "\")");
} else {
sql.append("\"" + geomName + "\"" );
}
}
private final void columnAttribute(StringBuffer sql, AttributeDescriptor attribute){
sql.append( "\"" );
sql.append( attribute.getLocalName() );
sql.append( "\"" );
}
/**
* Constructs FROM clause for featureType
*
* <p>
* sql: <code>FROM typeName</code>
* </p>
*
* @param sql
* @param typeName
*/
public void sqlFrom(StringBuffer sql, String typeName) {
sql.append(" FROM ");
sql.append(encodeTableName(typeName));
}
/**
* Constructs WHERE clause, if needed, for FILTER.
*
* <p>
* sql: <code>WHERE filter encoding</code>
* </p>
*
* @param sql DOCUMENT ME!
* @param preFilter DOCUMENT ME!
*
* @throws SQLEncoderException DOCUMENT ME!
*/
public void sqlWhere(StringBuffer sql, Filter preFilter)
throws SQLEncoderException {
if ((preFilter != null) || (preFilter == org.geotools.filter.Filter.NONE)) {
String where = encoder.encode(preFilter);
sql.append(" ");
sql.append(where);
}
}
/**
* Returns true if the WKB format is used to transfer geometries, false
* otherwise
*
*/
public boolean isWKBEnabled() {
return WKBEnabled;
}
/**
* If turned on, WKB will be used to transfer geometry data instead of WKT
*
* @param enabled
*/
public void setWKBEnabled(boolean enabled) {
WKBEnabled = enabled;
}
/**
* Enables the use of the bytea function to transfer faster WKB geometries
*/
public boolean isByteaEnabled() {
return byteaEnabled;
}
/**
* Enables/disables the use of the bytea function
* @param byteaEnable
*/
public void setByteaEnabled(boolean byteaEnable) {
byteaEnabled = byteaEnable;
}
/**
* Enables/disables schema name qualification.
*/
public void setSchemaEnabled(boolean schemaEnabled) {
this.schemaEnabled = schemaEnabled;
}
/**
* @return true if table names are prefixed with the containing schema.
*/
public boolean isSchemaEnabled() {
return schemaEnabled;
}
public String encodeTableName(String tableName) {
return schemaEnabled ?
"\"" + config.getDatabaseSchemaName() + "\".\"" + tableName + "\"" :
"\"" + tableName + "\"";
}
public String encodeColumnName(String columnName) {
return "\"" + columnName + "\"";
}
}