package org.geotools.caching.grid.featurecache.readers;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.caching.grid.featurecache.GridFeatureCache;
import org.geotools.caching.grid.spatialindex.GridSpatialIndex;
import org.geotools.caching.spatialindex.NodeIdentifier;
import org.geotools.caching.spatialindex.Region;
import org.geotools.data.DataStore;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultQuery;
import org.geotools.data.EmptyFeatureReader;
import org.geotools.data.FeatureReader;
import org.geotools.data.FeatureSource;
import org.geotools.data.MaxFeatureReader;
import org.geotools.data.Query;
import org.geotools.data.ReTypeFeatureReader;
import org.geotools.data.Transaction;
import org.geotools.data.crs.ReprojectFeatureReader;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.Hints;
import org.geotools.feature.CollectionListener;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.SchemaException;
import org.geotools.feature.collection.DelegateFeatureIterator;
import org.geotools.geometry.jts.LiteCoordinateSequenceFactory;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.opengis.feature.FeatureVisitor;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.spatial.BBOX;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.util.ProgressListener;
import com.vividsolutions.jts.geom.Envelope;
/**
* Feature collection that reads features from two sources, a cache and a feature source.
* <p>Responsible for reading features & caching features from the feature source.</p>
* <p>This is a read only feature collection.</p>
*
* @author Emily
*
*
* @source $URL$
*/
public class GridCachingFeatureCollection implements FeatureCollection<SimpleFeatureType, SimpleFeature>{
private static final int MAX_FILTER_SIZE = 4; //the maximum number of "and" statements allowed in filter
private static Logger logger = org.geotools.util.logging.Logging.getLogger("org.geotools.caching");
private static FilterFactory filterFactory = CommonFactoryFinder.getFilterFactory2(null);
private BBOX preFilter; //cache feature filter
private Filter postFilter; //cache post filter
private Query query; //query (may be null)
private GridFeatureCache grid; //feature cache
private FeatureSource<SimpleFeatureType, SimpleFeature> fs; //raw feature source
private FeatureType featureType ; //feature type associated with feature source
private FeatureType newFeatureType; //feature type associated with returned feature collection
private Set<Object> iterators; //list of the open iterators on the feature collection
private boolean cacheFeatures = false; //if features are cached
private MathTransform reproject = null; //reprojection to apply to features
private Collection<CollectionListener> listeners;
/**
* Creates a feature collection that will first
* read items from the cache then read any missing
* items from the feature source.
*
* @param preFilter the bounding filter of the request
* @param postFilter the complete request filter
* @param g
* @param fs
* @param cacheFeatures
*/
public GridCachingFeatureCollection(BBOX preFilter, Filter postFilter,
GridFeatureCache g, FeatureSource<SimpleFeatureType, SimpleFeature> fs, boolean cacheFeatures){
this.grid = g;
this.preFilter = preFilter;
this.postFilter = postFilter;
this.fs = fs;
this.cacheFeatures = cacheFeatures;
this.listeners = new HashSet<CollectionListener>();
iterators = new HashSet<Object>();
featureType = g.getSchema();
}
/**
* Creates a feature collection that will first read itmes from the cache
* and then read missing items from the feature source.
*
* @param preFilter the bounding filter of the request
* @param query the query
* @param g
* @param fs
* @param cacheFeatures
* @throws IOException
*/
public GridCachingFeatureCollection(BBOX preFilter, Query query, GridFeatureCache g,
FeatureSource<SimpleFeatureType, SimpleFeature> fs, boolean cacheFeatures) throws IOException{
this(preFilter, query.getFilter(), g, fs, cacheFeatures);
this.query = query;
//setup crs transformation
CoordinateReferenceSystem source = g.getSchema().getCoordinateReferenceSystem();
CoordinateReferenceSystem dest = null;
if (query.getCoordinateSystemReproject() != null) {
dest = query.getCoordinateSystemReproject();
} else if (query.getCoordinateSystem() != null) {
dest = query.getCoordinateSystem();
}
if (query.getCoordinateSystem() != null){
source = query.getCoordinateSystem();
}
if (dest != null && !dest.equals(source)){
try{
reproject = CRS.findMathTransform(source,dest);
}catch (FactoryException ex){
IOException newex = new IOException("Could not reproject data to "+ dest);
newex.initCause( ex );
throw newex;
}
}
//determine new feature type (with new geometry and filtered attributes)
this.newFeatureType = createSchema((SimpleFeatureType)featureType, query, dest);
//for now we can't sort queries
if (query.getSortBy() != null && query.getSortBy().length > 0){
throw new IOException("Sorting not supported by this feature collection.");
}
//and we also don't support starting at some given index
if (query.getStartIndex() != null && query.getStartIndex() > 0){
throw new IOException("Start index not supported by this feature collection.");
}
}
/**
* Creates the schema of the resulting feature collection
*/
private SimpleFeatureType createSchema(SimpleFeatureType current, Query query, CoordinateReferenceSystem destcrs){
try{
if (destcrs != null && !destcrs.equals(current.getCoordinateReferenceSystem())){
//need to change the projection of the schema
return DataUtilities.createSubType(current, query.getPropertyNames(), destcrs, query.getTypeName(), null);
}else{
if (query.retrieveAllProperties()){
//nothing to change
return current;
}else{
return DataUtilities.createSubType(current, query.getPropertyNames());
}
}
}catch (SchemaException ex){
logger.log(Level.SEVERE, "Error converting feature type to match query request.", ex);
}
return null;
}
/**
* Creates a feature collection that will only
* read items from the cache.
*
* @param e
* @param g
*/
public GridCachingFeatureCollection(Envelope e, GridFeatureCache g){
this.grid = g;
this.preFilter = filterFactory.bbox(g.getSchema().getGeometryDescriptor().getLocalName(), e.getMinX(), e.getMinY(), e.getMaxX(), e.getMaxY(), g.getSchema().getGeometryDescriptor().getCoordinateReferenceSystem().toString());
this.postFilter =filterFactory.bbox(g.getSchema().getGeometryDescriptor().getLocalName(), e.getMinX(), e.getMinY(), e.getMaxX(), e.getMaxY(), g.getSchema().getGeometryDescriptor().getCoordinateReferenceSystem().toString());
this.fs = null;
this.cacheFeatures = false;
this.listeners = new HashSet<CollectionListener>();
iterators = new HashSet<Object>();
featureType = g.getSchema();
}
/**
* Visits all features in the collection.
*/
public void accepts(FeatureVisitor visitor, ProgressListener progress) throws IOException {
FeatureIterator<SimpleFeature> feats = features();
try {
while( feats.hasNext() ) {
SimpleFeature f = feats.next();
visitor.visit(f);
}
} finally {
feats.close();
}
}
/**
* Closes the feature iterator
*/
public void close(FeatureIterator<SimpleFeature> close) {
try {
if (close != null) {
close.close(); //this will call the close(Iterator) method
}
} finally {
}
}
/**
* Close the iterator
*/
public void close(Iterator<SimpleFeature> close) {
if (close == null) return;
try{
closeIterator(close);
}catch (Throwable e){
logger.log(Level.WARNING, "Failed to close iterator", e);
}finally{
iterators.remove(close);
}
}
/**
* Determines if the feature collection contains the
* given object.
*/
public boolean contains(Object o) {
Iterator<SimpleFeature> e = null;
try {
e = iterator();
if (o == null) {
while (e.hasNext())
if (e.next() == null)
return true;
} else {
while (e.hasNext())
if (o.equals(e.next()))
return true;
}
return false;
} finally {
close(e);
}
}
/**
* Determines if the feature collection contains
* all objects in the collection
*/
public boolean containsAll(Collection<?> c) {
Iterator<?> e = c.iterator();
try {
while (e.hasNext())
if(!contains(e.next()))
return false;
return true;
} finally {
close( (Iterator<SimpleFeature>)e );
}
}
/**
* @returns a feature iterator that iterators over all features in the collection
*/
public FeatureIterator<SimpleFeature> features() {
FeatureIterator<SimpleFeature> iter = new DelegateFeatureIterator<SimpleFeature>(this, openIterator());
iterators.add(iter);
return iter;
}
/**
* Closes a iterator
* @param it
*/
private void closeIterator(Iterator<SimpleFeature> it){
if (it == null) return;
FeatureReaderFeatureIterator iter = ((FeatureReaderFeatureIterator)it);
try{
if (it.hasNext()){
//there are still features left so we cannot leave this area as registered
this.grid.unregister(iter.getMissingNodes());
}else if (iter.getMissingNodes().size() > 0){
this.grid.getIndex().flush();
}
}finally{
}
iter.close();
}
/**
* Opens a new iterator. This will lock the cache and nodes as required.
* @return
*/
private Iterator<SimpleFeature> openIterator(){
Iterator<SimpleFeature> it = null;
List<NodeIdentifier> missing = null;
List<NodeIdentifier> found = null;
FeatureReader<SimpleFeatureType, SimpleFeature> cacheFeatureReader = null;
FeatureReader<SimpleFeatureType, SimpleFeature> sourceFeatureReader = null;
Envelope env = new Envelope(preFilter.getMinX(), preFilter.getMaxX(), preFilter.getMinY(), preFilter.getMaxY());
this.grid.readLock();
try{
//get features from the cache and the source
List<NodeIdentifier>[] notcached = grid.matchNodeIds(env);
missing = notcached[0];
found = notcached[1];
//acquire read locks
acquireReadLocks(missing, found);
if (missing.size() == 0){
//all locks acquired
this.grid.readUnLock();
cacheFeatureReader = new GridCacheFeatureReader(found, (GridSpatialIndex)grid.getIndex());
}else{
//need a write lock; release all read locks
for( Iterator<NodeIdentifier> iterator = found.iterator(); iterator.hasNext(); ) {
NodeIdentifier nodeid = (NodeIdentifier) iterator.next();
nodeid.readUnLock();
}
this.grid.readUnLock();
this.grid.writeLock();
try{
//re-get read locks
acquireReadLocks(missing, found);
// acquire write locks
if (missing.size() > 0 && fs != null) {
acquireWriteLocks(missing, found);
}
// register working area
if (missing.size() > 0 && fs != null) {
this.grid.register(missing);
}
}finally{
this.grid.writeUnLock();
}
if (fs != null) {
// note that for performance reasons this query might actually return
// features from an area larger than we are interested in caching
DefaultQuery dq = new DefaultQuery(featureType.getName().getLocalPart(), this.createFilter(missing));
dq.setCoordinateSystem(featureType.getCoordinateReferenceSystem());
if (query != null) {
dq.setHints(query.getHints());
dq.setHandle(query.getHandle());
} else {
dq.setHints(new Hints(Hints.JTS_COORDINATE_SEQUENCE_FACTORY,new LiteCoordinateSequenceFactory()));
}
try{
sourceFeatureReader = ((DataStore) fs.getDataStore()).getFeatureReader(dq,Transaction.AUTO_COMMIT);
}catch (Exception ex){
//nothing to get
missing.clear();
sourceFeatureReader = null;
}
} else {
missing.clear(); // we didn't get anything so we don't want to register these as write locks
}
if (found.size() > 0) {
cacheFeatureReader = new GridCacheFeatureReader(found, (GridSpatialIndex) grid.getIndex());
}
}
boolean localcache = cacheFeatures;
if (query != null && query.getMaxFeatures() != Integer.MAX_VALUE){
localcache = false;
}
FeatureReader<SimpleFeatureType, SimpleFeature> fr = null;
if (sourceFeatureReader != null && cacheFeatureReader != null){
fr = new CombiningCachingFeatureReader(cacheFeatureReader, sourceFeatureReader, false, localcache, grid.getIndex(), this.postFilter);
}else if (sourceFeatureReader != null && cacheFeatureReader == null){
fr = new CombiningCachingFeatureReader(new EmptyFeatureReader<SimpleFeatureType, SimpleFeature>((SimpleFeatureType)featureType), sourceFeatureReader, false, localcache, grid.getIndex(), this.postFilter);
}else if (sourceFeatureReader == null && cacheFeatureReader != null){
fr = new CombiningCachingFeatureReader(cacheFeatureReader, new EmptyFeatureReader<SimpleFeatureType, SimpleFeature>((SimpleFeatureType)featureType), false, false, grid.getIndex(), this.postFilter);
}else{
fr = new CombiningCachingFeatureReader(new EmptyFeatureReader<SimpleFeatureType, SimpleFeature>((SimpleFeatureType)featureType), new EmptyFeatureReader<SimpleFeatureType, SimpleFeature>((SimpleFeatureType)featureType), false, false, grid.getIndex(), this.postFilter);
}
it = new FeatureReaderFeatureIterator(wrapFeatureReader(fr), missing, found);
} catch (Exception ex) {
logger.log(Level.SEVERE, "Failed to create feature collection iterator.", ex);
if (missing != null){
for( Iterator<NodeIdentifier> iterator = missing.iterator(); iterator.hasNext(); ) {
NodeIdentifier nodeid = (NodeIdentifier) iterator.next();
nodeid.writeUnLock();
}
}
if (found != null){
for( Iterator<NodeIdentifier> iterator = found.iterator(); iterator.hasNext(); ) {
NodeIdentifier nodeid = (NodeIdentifier) iterator.next();
nodeid.readUnLock();
}
}
if (missing != null){
this.grid.unregister(missing);
}
}finally{
try{
//ensure we are unlocked
this.grid.readUnLock();
}catch (Exception ex){}
}
return it;
}
/**
* This function acquire write locks for everything in the missing array.
* Once the write lock is acquired if the node is valid it is converted to
* a read lock.
*
* @param missing
* @param found
* @throws Exception
*/
private void acquireWriteLocks( List<NodeIdentifier> missing, List<NodeIdentifier> found )
throws Exception {
for( Iterator<NodeIdentifier> iterator = missing.iterator(); iterator.hasNext(); ) {
NodeIdentifier nodeid = (NodeIdentifier) iterator.next();
try {
nodeid.writeLock();
}catch (Exception ex){
//error occurred; remove from list
iterator.remove();
logger.log(Level.SEVERE, "Could not acquire necessary write locks.", ex);
continue;
}
if (nodeid.isValid()) {
// might have been validated by previous thread; so lets make sure we have the correct queue
nodeid.readLock();
found.add(nodeid);
nodeid.writeUnLock();
iterator.remove();
}
}
}
/**
* This function acquires reads locks for everything in the missing array.
* Once the read lock is acquired if the node is not valid it is added
* to the missing array (write locks are not acquired here).
*
* @param missing
* @param found
*/
private void acquireReadLocks( List<NodeIdentifier> missing, List<NodeIdentifier> found ) {
for( Iterator<NodeIdentifier> iterator = found.iterator(); iterator.hasNext(); ) {
NodeIdentifier nodeid = (NodeIdentifier) iterator.next();
try {
nodeid.readLock();
} catch (Exception ex) {
// remove from list
logger.log(Level.SEVERE, "Could not acquire necessary read locks.", ex);
iterator.remove();
continue;
}
if (!nodeid.isValid()) {
// this node is no longer valid add to missing list
nodeid.readUnLock();
missing.add(nodeid);
iterator.remove();
}
}
}
/**
* Wraps a feature reader in a reproject and retype feature reader
* if necessary (as determined by query used in collection).
*
* @param reader
* @return
*/
private FeatureReader<SimpleFeatureType, SimpleFeature> wrapFeatureReader(FeatureReader<SimpleFeatureType, SimpleFeature> reader){
//creates a reproject feature reader
FeatureReader<SimpleFeatureType, SimpleFeature> newfr = reader;
if (query != null && query.getMaxFeatures() != Integer.MAX_VALUE){
newfr = new MaxFeatureReader<SimpleFeatureType, SimpleFeature>(newfr, query.getMaxFeatures());
}
if (this.reproject != null){
newfr = new ReprojectFeatureReader(newfr, (SimpleFeatureType) featureType, this.reproject);
}
//creates a re-type feature reader
if (this.newFeatureType != null && !this.newFeatureType.equals(this.featureType)){
newfr = new ReTypeFeatureReader(newfr, (SimpleFeatureType)this.newFeatureType);
}
return newfr;
}
/**
* Creates a filter from a collection of shapes. If there are more than 4 shapes
* then all the shapes are just expanded to include all. This means that you may end up requesting
* more features than are identified by the shapes.
*
* @param shapes
* @return
*/
private Filter createFilter( Collection<NodeIdentifier> shapes ) {
String geometryname = this.featureType.getGeometryDescriptor().getLocalName();
String srs = this.featureType.getGeometryDescriptor().getCoordinateReferenceSystem().toString();
ArrayList<Filter> filters = new ArrayList<Filter>(shapes.size());
if (shapes.size() < MAX_FILTER_SIZE) {
for( Iterator<NodeIdentifier> iterator = shapes.iterator(); iterator.hasNext(); ) {
Region r = (Region) iterator.next().getShape();
Filter bbox = filterFactory.bbox(geometryname, r.getLow(0), r.getLow(1), r.getHigh(0), r.getHigh(1), srs);
filters.add(bbox);
}
return filterFactory.or(filters);
} else {
Region area = null;
for( Iterator<NodeIdentifier> iterator = shapes.iterator(); iterator.hasNext(); ) {
Region r = (Region) iterator.next().getShape();
if (area == null) {
area = new Region(r);
} else {
area = area.combinedRegion(r);
}
}
return filterFactory.bbox(geometryname, area.getLow(0), area.getLow(1),area.getHigh(0), area.getHigh(1), srs);
}
}
/**
* @throws UnsupportedOperationException
*/
public ReferencedEnvelope getBounds() {
throw new UnsupportedOperationException("Bounds of this collection not known");
}
public String getID() {
return "cachingFeatureCollection";
}
public SimpleFeatureType getSchema() {
return (SimpleFeatureType) newFeatureType;
}
public Iterator<SimpleFeature> iterator() {
Iterator<SimpleFeature> it = openIterator();
iterators.add(it);
return it;
}
/**
* @throws UnsupportedOperationException
*/
public Object[] toArray() {
throw new UnsupportedOperationException("Cannot convert to array.");
}
/**
* @throws UnsupportedOperationException
*/
public <O> O[] toArray(O[] a) {
throw new UnsupportedOperationException("Cannot convert to array.");
}
/**
* @throws UnsupportedOperationException
*/
public boolean isEmpty() {
throw new UnsupportedOperationException("Size unknown.");
}
/**
* Determines the size of the feature collection
*/
public int size() {
FeatureIterator<SimpleFeature> fi = features();
int cnt = 0;
try {
while (fi.hasNext()) {
fi.next();
cnt++;
}
} finally {
close(fi);
}
return cnt;
}
public void removeListener(CollectionListener listener) throws NullPointerException {
listeners.remove(listener);
}
public void addListener(CollectionListener listener) throws NullPointerException {
listeners.add(listener);
}
/**
* @throws UnsupportedOperationException
*/
public void purge() {
throw new UnsupportedOperationException("Cannot modify this feature collection");
}
/**
* @throws UnsupportedOperationException
*/
public boolean remove(Object o) {
throw new UnsupportedOperationException("Cannot modify this feature collection");
}
/**
* @throws UnsupportedOperationException
*/
public boolean removeAll(Collection<?> c) {
throw new UnsupportedOperationException("Cannot modify this feature collection");
}
/**
* @throws UnsupportedOperationException
*/
public boolean retainAll(Collection<?> c) {
throw new UnsupportedOperationException("Cannot modify this feature collection");
}
/**
* @throws UnsupportedOperationException
*/
public FeatureCollection<SimpleFeatureType, SimpleFeature> subCollection(Filter filter) {
throw new UnsupportedOperationException("Cannot modify this feature collection");
}
/**
* @throws UnsupportedOperationException
*/
public FeatureCollection<SimpleFeatureType, SimpleFeature> sort(SortBy order) {
throw new UnsupportedOperationException("Cannot modify this feature collection");
}
/**
* @throws UnsupportedOperationException
*/
public boolean add(SimpleFeature obj) {
throw new UnsupportedOperationException("Cannot add features to this feature collection");
}
/**
* @throws UnsupportedOperationException
*/
public boolean addAll(Collection<? extends SimpleFeature> collection) {
throw new UnsupportedOperationException("Cannot add features to this feature collection");
}
/**
* @throws UnsupportedOperationException
*/
public boolean addAll(FeatureCollection<? extends SimpleFeatureType, ? extends SimpleFeature> resource) {
throw new UnsupportedOperationException("Cannot add features to this feature collection");
}
/**
* @throws UnsupportedOperationException
*/
public void clear() {
throw new UnsupportedOperationException("Cannot clear this collection");
}
/**
* A wrapper around a feature reader that
* implements the iterator interface.
*
*/
class FeatureReaderFeatureIterator implements Iterator<SimpleFeature>{
private FeatureReader<SimpleFeatureType, SimpleFeature> reader = null;
private Collection<NodeIdentifier> writelocks;
private Collection<NodeIdentifier> readlocks;
public FeatureReaderFeatureIterator(FeatureReader<SimpleFeatureType, SimpleFeature> fr, Collection<NodeIdentifier> writeLocks, Collection<NodeIdentifier> readLocks){
this.reader = fr;
this.writelocks = writeLocks;
this.readlocks = readLocks;
}
private void releaseLocks(){
try {
for (Iterator<NodeIdentifier> iterator = writelocks.iterator(); iterator.hasNext();) {
NodeIdentifier type = (NodeIdentifier) iterator.next();
try{
type.writeUnLock();
}catch (Exception ex){
// logger.log(Level.SEVERE, "Could not release write lock.", ex);
}
}
} catch (Exception ex) {
logger.log(Level.SEVERE, "Could not release write locks.", ex);
}
try {
for (Iterator<NodeIdentifier> iterator = readlocks.iterator(); iterator.hasNext();) {
NodeIdentifier type = (NodeIdentifier) iterator.next();
try{
type.readUnLock();
}catch (Exception ex){
// logger.log(Level.SEVERE, "Could not release read lock.", ex);
}
}
} catch (Exception ex) {
logger.log(Level.SEVERE, "Could not release read locks.", ex);;
}
}
/**
* Returns the nodes that have been write locked. These
* are the nodes that are currently being written
* to.
*
* @return
*/
public Collection<NodeIdentifier> getMissingNodes(){
return this.writelocks;
}
public boolean hasNext() {
try {
if (reader == null) return false;
if (reader.hasNext()){
return true;
}else{
return false;
}
} catch (IOException ex) {
logger.log(Level.SEVERE, "No more items.", ex);
close();
return false;
}
}
public SimpleFeature next() throws NoSuchElementException {
if (reader == null){
throw new NoSuchElementException("Iterator has been closed.");
}
try {
return (SimpleFeature)reader.next();
} catch (IOException ex) {
close();
NoSuchElementException problem = new NoSuchElementException("Could not obtain the next feature:" + ex);
problem.initCause( ex );
throw problem;
}
}
public void remove() {
throw new UnsupportedOperationException("Modifications of the contents of this iterator is not supported.");
}
public void close(){
releaseLocks();
if (reader == null) return;
try{
reader.close();
}catch(IOException ex){
logger.log(Level.SEVERE, "Error closing reader.", ex);
}
reader = null;
}
}
}