/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 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.jdbc;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.data.FeatureReader;
import org.geotools.data.Transaction;
import org.geotools.factory.Hints;
import org.geotools.feature.IllegalAttributeException;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.filter.identity.FeatureIdImpl;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.util.Converters;
import org.geotools.util.logging.Logging;
import org.opengis.feature.FeatureFactory;
import org.opengis.feature.GeometryAttribute;
import org.opengis.feature.Property;
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.feature.type.Name;
import org.opengis.filter.identity.FeatureId;
import org.opengis.geometry.BoundingBox;
import com.vividsolutions.jts.geom.CoordinateSequenceFactory;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
/**
* Reader for jdbc datastore
*
* @author Justin Deoliveira, The Open Plannign Project.
*
*
*
*
* @source $URL$
*/
public class JDBCFeatureReader implements FeatureReader<SimpleFeatureType, SimpleFeature> {
protected static final Logger LOGGER = Logging.getLogger(JDBCFeatureReader.class);
/**
* When true, the stack trace that created a reader that wasn't closed is recorded and then
* printed out when warning the user about this.
*/
protected static final Boolean TRACE_ENABLED = "true".equalsIgnoreCase(System.getProperty("gt2.jdbc.trace"));
/**
* The feature source the reader originated from.
*/
protected JDBCFeatureSource featureSource;
/**
* the datastore
*/
protected JDBCDataStore dataStore;
/**
* schema of features
*/
protected SimpleFeatureType featureType;
/**
* geometry factory used to create geometry objects
*/
protected GeometryFactory geometryFactory;
/**
* hints
*/
protected Hints hints;
/**
* current transaction
*/
protected Transaction tx;
/**
* flag indicating if the iterator has another feature
*/
protected Boolean next;
/**
* feature builder
*/
protected SimpleFeatureBuilder builder;
/**
* The primary key
*/
protected PrimaryKey pkey;
/**
* statement,result set that is being worked from.
*/
protected Statement st;
protected ResultSet rs;
protected Connection cx;
protected Exception tracer;
protected String[] columnNames;
/**
* offset/column index to start reading from result set
*/
protected int offset = 0;
public JDBCFeatureReader( String sql, Connection cx, JDBCFeatureSource featureSource, SimpleFeatureType featureType, Hints hints )
throws SQLException {
init( featureSource, featureType, hints );
//create the result set
this.cx = cx;
st = cx.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
st.setFetchSize(featureSource.getDataStore().getFetchSize());
((BasicSQLDialect)featureSource.getDataStore().getSQLDialect()).onSelect(st, cx, featureType);
rs = st.executeQuery(sql);
}
public JDBCFeatureReader( PreparedStatement st, Connection cx, JDBCFeatureSource featureSource, SimpleFeatureType featureType, Hints hints )
throws SQLException {
init( featureSource, featureType, hints );
//create the result set
this.cx = cx;
this.st = st;
((PreparedStatementSQLDialect)featureSource.getDataStore().getSQLDialect()).onSelect(st, cx, featureType);
rs = st.executeQuery();
}
public JDBCFeatureReader(ResultSet rs, Connection cx, int offset, JDBCFeatureSource featureSource,
SimpleFeatureType featureType, Hints hints) throws SQLException {
init(featureSource, featureType, hints);
this.cx = cx;
this.st = rs.getStatement();
this.rs = rs;
this.offset = offset;
}
protected void init( JDBCFeatureSource featureSource, SimpleFeatureType featureType, Hints hints ) {
// init the tracer if we need to debug a connection leak
if(TRACE_ENABLED) {
tracer = new Exception();
tracer.fillInStackTrace();
}
// init base fields
this.featureSource = featureSource;
this.dataStore = featureSource.getDataStore();
this.featureType = featureType;
this.tx = featureSource.getTransaction();
this.hints = hints;
//grab a geometry factory... check for a special hint
geometryFactory = (GeometryFactory) hints.get(Hints.JTS_GEOMETRY_FACTORY);
if (geometryFactory == null) {
// look for a coordinate sequence factory
CoordinateSequenceFactory csFactory =
(CoordinateSequenceFactory) hints.get(Hints.JTS_COORDINATE_SEQUENCE_FACTORY);
if (csFactory != null) {
geometryFactory = new GeometryFactory(csFactory);
}
}
if (geometryFactory == null) {
// fall back on one privided by datastore
geometryFactory = dataStore.getGeometryFactory();
}
// create a feature builder using the factory hinted or the one coming
// from the datastore
FeatureFactory ff = (FeatureFactory) hints.get(Hints.FEATURE_FACTORY);
if(ff == null)
ff = featureSource.getDataStore().getFeatureFactory();
builder = new SimpleFeatureBuilder(featureType, ff);
// find the primary key
try {
pkey = dataStore.getPrimaryKey(featureType);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public JDBCFeatureReader( JDBCFeatureReader other ) {
this.featureType = other.featureType;
this.dataStore = other.dataStore;
this.featureSource = other.featureSource;
this.tx = other.tx;
this.hints = other.hints;
this.geometryFactory = other.geometryFactory;
this.builder = other.builder;
this.st = other.st;
this.rs = other.rs;
}
public void setNext(Boolean next) {
this.next = next;
}
public SimpleFeatureType getFeatureType() {
return featureType;
}
public PrimaryKey getPrimaryKey() {
return pkey;
}
public boolean hasNext() throws IOException {
ensureOpen();
if (next == null) {
try {
next = Boolean.valueOf(rs.next());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return next.booleanValue();
}
protected void ensureNext() {
if (next == null) {
throw new IllegalStateException("Must call hasNext before calling next");
}
}
protected void ensureOpen() throws IOException {
if ( rs == null ) {
throw new IOException( "reader already closed" );
}
}
public SimpleFeature next() throws IOException, IllegalArgumentException,
NoSuchElementException {
try {
ensureOpen();
if(!hasNext()) {
throw new NoSuchElementException("No more features in this reader, you should call " +
"hasNext() to check for feature availability");
}
//grab the connection
Connection cx;
try {
cx = st.getConnection();
}
catch (SQLException e) {
throw (IOException) new IOException().initCause(e);
}
// figure out the fid
String fid;
try {
fid = dataStore.encodeFID(pkey,rs,offset);
if (fid == null) {
//fid could be null during an outer join
return null;
}
// wrap the fid in the type name
fid = featureType.getTypeName() + "." + fid;
} catch (Exception e) {
throw new RuntimeException("Could not determine fid from primary key", e);
}
// round up attributes
final int attributeCount = featureType.getAttributeCount();
int[] attributeRsIndex = buildAttributeRsIndex();
for(int i = 0; i < attributeCount; i++) {
AttributeDescriptor type = featureType.getDescriptor(i);
try {
Object value = null;
// is this a geometry?
if (type instanceof GeometryDescriptor) {
GeometryDescriptor gatt = (GeometryDescriptor) type;
//read the geometry
try {
value = dataStore.getSQLDialect()
.decodeGeometryValue(gatt, rs, offset+attributeRsIndex[i],
geometryFactory, cx);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (value != null) {
//check to see if a crs was set
Geometry geometry = (Geometry) value;
if ( geometry.getUserData() == null ) {
//if not set, set from descriptor
geometry.setUserData( gatt.getCoordinateReferenceSystem() );
}
}
} else {
value = rs.getObject(offset+attributeRsIndex[i]);
}
// they value may need conversion. We let converters chew the initial
// value towards the target type, if the result is not the same as the
// original, then a conversion happened and we may want to report it to the
// user (being the feature type reverse engineerd, it's unlikely a true
// conversion will be needed)
if(value != null) {
Class binding = type.getType().getBinding();
Object converted = Converters.convert(value, binding);
if(converted != null && converted != value) {
value = converted;
if (dataStore.getLogger().isLoggable(Level.FINER)) {
String msg = value + " is not of type " + binding.getName()
+ ", attempting conversion";
dataStore.getLogger().finer(msg);
}
}
}
builder.add(value);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
// create the feature
try {
return builder.buildFeature(fid);
} catch (IllegalAttributeException e) {
throw new RuntimeException(e);
}
} finally {
// reset the next flag. We do this in a finally block to make sure we
// move to the next record no matter what, if the current one could
// not be read there is no salvation for it anyways
next = null;
}
}
/**
* Builds an array containing the position in the result set for each attribute.
* It takes into account that rs positions start by one, about the exposed primary keys,
* and the fact that exposed pk can be only partially selected in the output
* @return
*/
private int[] buildAttributeRsIndex() {
LinkedHashSet<String> pkColumns = dataStore.getColumnNames(pkey);
List<String> pkColumnsList = new ArrayList<String>(pkColumns);
int[] indexes = new int[featureType.getAttributeCount()];
int exposedPks = 0;
for(int i = 0; i < indexes.length; i++) {
String attName = featureType.getDescriptor(i).getLocalName();
if(pkColumns.contains(attName)) {
indexes[i] = pkColumnsList.indexOf(attName) + 1;
exposedPks++;
} else {
indexes[i] = i + pkColumns.size() - exposedPks + 1;
}
}
return indexes;
}
public void close() throws IOException {
if ( dataStore != null ) {
//clean up
dataStore.closeSafe( rs );
dataStore.closeSafe( st );
dataStore.releaseConnection(cx, featureSource.getState() );
}
else {
//means we are already closed... should we throw an exception?
}
cleanup();
}
/**
* Cleans up the reader state without closing the accessory resultset, statement
* and connection. Use only if the above are shared with another object that will
* take care of closing them.
*/
protected void cleanup() {
//throw away state
rs = null;
st = null;
dataStore = null;
featureSource = null;
featureType = null;
geometryFactory = null;
tx = null;
hints = null;
next = null;
builder = null;
tracer = null;
}
@Override
protected void finalize() throws Throwable {
if(dataStore != null) {
LOGGER.warning("There is code leaving feature readers/iterators open, this is leaking statements and connections!");
if(TRACE_ENABLED) {
LOGGER.log(Level.WARNING, "The unclosed reader originated on this stack trace", tracer);
}
close();
}
}
/**
* Feature wrapper around a result set.
*/
protected class ResultSetFeature implements SimpleFeature {
/**
* result set
*/
ResultSet rs;
/**
* connection
*/
Connection cx;
/**
* primary key
*/
PrimaryKey key;
/**
* updated values
* */
Object[] values;
/**
* fid
*/
FeatureId fid;
/**
* dirty flags
*/
boolean[] dirty;
/**
* Marks this feature as "new", about to be inserted
*/
boolean newFeature;
/**
* name index
*/
HashMap<String, Integer> index;
/**
* user data
*/
HashMap<Object, Object> userData = new HashMap<Object, Object>();
/**
* true if primary keys are not returned (the default is false)
*/
boolean exposePrimaryKeys;
ResultSetFeature(ResultSet rs, Connection cx) throws SQLException, IOException {
this.rs = rs;
this.cx = cx;
//get the result set metadata
ResultSetMetaData md = rs.getMetaData();
//get the primary key, ensure its not contained in the values
key = dataStore.getPrimaryKey(featureType);
int count = md.getColumnCount();
columnNames=new String[count];
exposePrimaryKeys = featureSource.getState().isExposePrimaryKeyColumns();
for (int i = 0; i < md.getColumnCount(); i++) {
String columnName =md.getColumnName(i + 1);
columnNames[i]=columnName;
if(!exposePrimaryKeys) {
for ( PrimaryKeyColumn col : key.getColumns() ) {
if (col.getName().equals(columnName)) {
count--;
break;
}
}
}
}
//set up values
values = new Object[count];
dirty = new boolean[values.length];
//set up name lookup
index = new HashMap<String, Integer>();
int offset = 0;
O: for (int i = 0; i < md.getColumnCount(); i++) {
if(!exposePrimaryKeys) {
for( PrimaryKeyColumn col : key.getColumns() ) {
if ( col.getName().equals( md.getColumnName(i+1))) {
offset++;
continue O;
}
}
}
index.put(md.getColumnName(i + 1), i - offset);
}
}
public void init(String fid) {
// mark as new according to the fid
newFeature = fid == null;
//clear values
for (int i = 0; i < values.length; i++) {
values[i] = null;
dirty[i] = false;
}
this.fid = SimpleFeatureBuilder.createDefaultFeatureIdentifier(fid);
}
public void init() throws SQLException, IOException {
//get fid
//PrimaryKey pkey = dataStore.getPrimaryKey(featureType);
//TODO: factory fid prefixing out
init(featureType.getTypeName() + "." + dataStore.encodeFID( key, rs, offset ));
}
public SimpleFeatureType getFeatureType() {
return featureType;
}
public SimpleFeatureType getType() {
return featureType;
}
public FeatureId getIdentifier() {
return fid;
}
public String getID() {
return fid.getID();
}
public void setID( String id ) {
((FeatureIdImpl)fid).setID(id);
}
public Object getAttribute(String name) {
return getAttribute(index.get(name));
}
public Object getAttribute(Name name) {
return getAttribute(name.getLocalPart());
}
public Object getAttribute(int index) throws IndexOutOfBoundsException {
return getAttributeInternal( index, mapToResultSetIndex(index) );
}
private int mapToResultSetIndex( int index ) {
//map the index to result set
int rsindex = index;
for ( int i = 0; i <= index; i++ ) {
if(!exposePrimaryKeys) {
for( PrimaryKeyColumn col : key.getColumns() ) {
if ( col.getName().equals( columnNames[i])) {
rsindex++;
break;
}
}
}
}
rsindex++;
return rsindex;
}
private Object getAttributeInternal( int index, int rsindex ) {
if (!newFeature && values[index] == null && !dirty[index]) {
synchronized (this) {
try {
if (!newFeature && values[index] == null && !dirty[index]) {
//load the value from the result set, check the case
// in which its a geometry, this case the dialect needs
// to read it
AttributeDescriptor att = featureType.getDescriptor(index);
if ( att instanceof GeometryDescriptor ) {
GeometryDescriptor gatt = (GeometryDescriptor) att;
values[index] = dataStore.getSQLDialect()
.decodeGeometryValue( gatt, rs, rsindex, dataStore.getGeometryFactory(), st.getConnection() );
}
else {
values[index] = rs.getObject( rsindex );
}
}
} catch (IOException e ) {
throw new RuntimeException( e );
} catch (SQLException e) {
//do not throw exception because of insert mode
//TODO: set a flag for insert vs update
//throw new RuntimeException( e );
values[index] = null;
}
}
}
return values[index];
}
public void setAttribute(String name, Object value) {
dataStore.getLogger().fine("Setting " + name + " to " + value);
int i = index.get(name);
setAttribute(i, value);
}
public void setAttribute(Name name, Object value) {
setAttribute(name.getLocalPart(), value);
}
public void setAttribute(int index, Object value)
throws IndexOutOfBoundsException {
dataStore.getLogger().fine("Setting " + index + " to " + value);
values[index] = value;
dirty[index] = true;
}
public void setAttributes(List<Object> values) {
for (int i = 0; i < values.size(); i++) {
setAttribute(i, values.get(i));
}
}
public int getAttributeCount() {
return values.length;
}
public boolean isDirty(int index) {
return dirty[index];
}
public boolean isDirrty(String name) {
return isDirty(index.get(name));
}
public void close() {
rs = null;
cx = null;
columnNames=null;
}
public List<Object> getAttributes() {
throw new UnsupportedOperationException();
}
public Object getDefaultGeometry() {
GeometryDescriptor defaultGeometry = featureType.getGeometryDescriptor();
return defaultGeometry != null ? getAttribute( defaultGeometry.getName() ) : null;
}
public void setAttributes(Object[] object) {
throw new UnsupportedOperationException();
}
public void setDefaultGeometry(Object defaultGeometry) {
GeometryDescriptor descriptor = featureType.getGeometryDescriptor();
setAttribute(descriptor.getName(), defaultGeometry );
}
public BoundingBox getBounds() {
Object obj = getDefaultGeometry();
if( obj instanceof Geometry ){
Geometry geometry = (Geometry) obj;
return new ReferencedEnvelope( geometry.getEnvelopeInternal(), featureType.getCoordinateReferenceSystem() );
}
return new ReferencedEnvelope( featureType.getCoordinateReferenceSystem() );
}
public GeometryAttribute getDefaultGeometryProperty() {
throw new UnsupportedOperationException();
}
public void setDefaultGeometryProperty(GeometryAttribute defaultGeometry) {
throw new UnsupportedOperationException();
}
public Collection<Property> getProperties() {
throw new UnsupportedOperationException();
}
public Collection<Property> getProperties(Name name) {
throw new UnsupportedOperationException();
}
public Collection<Property> getProperties(String name) {
throw new UnsupportedOperationException();
}
public Property getProperty(Name name) {
throw new UnsupportedOperationException();
}
public Property getProperty(String name) {
throw new UnsupportedOperationException();
}
public Collection<?extends Property> getValue() {
throw new UnsupportedOperationException();
}
public void setValue(Collection<Property> value) {
throw new UnsupportedOperationException();
}
public AttributeDescriptor getDescriptor() {
throw new UnsupportedOperationException();
}
public Name getName() {
throw new UnsupportedOperationException();
}
public Map<Object, Object> getUserData() {
return userData;
}
public boolean isNillable() {
throw new UnsupportedOperationException();
}
public void setValue(Object value) {
throw new UnsupportedOperationException();
}
public void validate() {
}
}
}