/* * 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.store; import java.io.IOException; import java.lang.reflect.Constructor; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.geotools.data.DataUtilities; import org.geotools.data.DefaultQuery; import org.geotools.data.FeatureListener; import org.geotools.data.FeatureReader; import org.geotools.data.FeatureSource; import org.geotools.data.FilteringFeatureReader; import org.geotools.data.MaxFeatureReader; import org.geotools.data.Query; import org.geotools.data.QueryCapabilities; import org.geotools.data.ReTypeFeatureReader; import org.geotools.data.ResourceInfo; import org.geotools.data.Transaction; import org.geotools.data.crs.ReprojectFeatureReader; import org.geotools.factory.Hints; import org.geotools.feature.FeatureCollection; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.filter.function.Collection_AverageFunction; import org.geotools.filter.function.Collection_BoundsFunction; import org.geotools.filter.function.Collection_CountFunction; import org.geotools.filter.function.Collection_MaxFunction; import org.geotools.filter.function.Collection_UniqueFunction; import org.geotools.filter.visitor.PropertyNameResolvingVisitor; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.util.NullProgressListener; import org.opengis.feature.FeatureVisitor; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.Name; import org.opengis.filter.Filter; import org.opengis.filter.sort.SortBy; import org.opengis.referencing.crs.CoordinateReferenceSystem; /** * Abstract implementation of FeatureSource. * <p> * This feature source works off of operations provided by {@link FeatureCollection}. * Individual FeatureCollection<SimpleFeatureType, SimpleFeature> implementations are provided by subclasses: * <ul> * {@link #all(ContentState)}: Access to entire dataset * {@link #filtered(ContentState, Filter)}: Access to filtered dataset * </ul> * </p> * <p> * Even though a feature source is read-only, this class is transaction aware. * (see {@link #setTransaction(Transaction)}. The transaction is taken into * account during operations such as {@link #getCount()} and {@link #getBounds()} * since these values may be affected by another operation (like writing to * a FeautreStore) working against the same transaction. * </p> * <p> * Subclasses must also implement the {@link #buildFeatureType()} method which * builds the schema for the feature source. * </p> * * @author Jody Garnett, Refractions Research Inc. * @author Justin Deoliveira, The Open Planning Project * * @source $URL$ */ public abstract class ContentFeatureSource implements FeatureSource<SimpleFeatureType, SimpleFeature> { /** * The entry for the feature source. */ protected ContentEntry entry; /** * The transaction to work from */ protected Transaction transaction; /** * hints */ protected Set<Hints.Key> hints; /** * The query defining the feature source */ protected Query query; /** * cached feature type (only set if this instance is a view) */ protected SimpleFeatureType schema; /** * The query capabilities returned by this feature source */ protected QueryCapabilities queryCapabilities; /** * Creates the new feature source from a query. * <p> * The <tt>query</tt> is taken into account for any operations done against * the feature source. For example, when getReader(Query) is called the * query specified is "joined" to the query specified in the constructor. * The <tt>query</tt> parameter may be <code>null</code> to specify that the * feature source represents the entire set of features. * </p> */ public ContentFeatureSource(ContentEntry entry, Query query) { this.entry = entry; this.query = query; //set up hints hints = new HashSet<Hints.Key>(); hints.add( Hints.JTS_GEOMETRY_FACTORY ); hints.add( Hints.JTS_COORDINATE_SEQUENCE_FACTORY ); //add subclass specific hints addHints( hints ); //make hints unmodifiable hints = Collections.unmodifiableSet( hints ); } /** * The entry for the feature source. */ public ContentEntry getEntry() { return entry; } /** * The current transaction the feature source is working against. * <p> * This transaction is used to derive the state for the feature source. A * <code>null</code> value for a transaction represents the auto commit * transaction: {@link Transaction#AUTO_COMMIT}. * </p> * @see {@link #getState()}. */ public Transaction getTransaction() { return transaction; } /** * Sets the current transaction the feature source is working against. * <p> * <tt>transaction</tt> may be <code>null</code>. This signifies that the * auto-commit transaction is used: {@link Transaction#AUTO_COMMIT}. * </p> * @param transaction The new transaction, or <code>null</code>. */ public void setTransaction(Transaction transaction) { this.transaction = transaction; } /** * The current state for the feature source. * <p> * This value is derived from current transaction of the feature source. * </p> * * @see {@link #setTransaction(Transaction)}. */ public ContentState getState() { return entry.getState(transaction); } /** * The datastore that this feature source originated from. * <p> * Subclasses may wish to extend this method in order to type narrow its * return type. * </p> */ public ContentDataStore getDataStore() { return entry.getDataStore(); } /** * Indicates if this feature source is actually a view. */ public final boolean isView() { return query != null && query != Query.ALL; } /** * A default ResourceInfo with a generic description. * <p> * Subclasses should override to provide an explicit ResourceInfo * object for their content. * @return description of features contents */ public ResourceInfo getInfo() { return new ResourceInfo(){ final Set<String> words = new HashSet<String>(); { words.add("features"); words.add( ContentFeatureSource.this.getSchema().getTypeName() ); } public ReferencedEnvelope getBounds() { try { return ContentFeatureSource.this.getBounds(); } catch (IOException e) { return null; } } public CoordinateReferenceSystem getCRS() { return ContentFeatureSource.this.getSchema().getCoordinateReferenceSystem(); } public String getDescription() { return null; } public Set<String> getKeywords() { return words; } public String getName() { return ContentFeatureSource.this.getSchema().getTypeName(); } public URI getSchema() { Name name = ContentFeatureSource.this.getSchema().getName(); URI namespace; try { namespace = new URI( name.getNamespaceURI() ); return namespace; } catch (URISyntaxException e) { return null; } } public String getTitle() { Name name = ContentFeatureSource.this.getSchema().getName(); return name.getLocalPart(); } }; } /** * Returns the same name than the feature type (ie, * {@code getSchema().getName()} to honor the simple feature land common * practice of calling the same both the Features produces and their types * * @since 2.5 * @see FeatureSource#getName() */ public Name getName() { return getSchema().getName(); } /** * Returns the feature type or the schema of the feature source. * <p> * This method delegates to {@link #buildFeatureType()}, which must be * implemented by subclasses. The result is cached in * {@link ContentState#getFeatureType()}. * </p> */ public final SimpleFeatureType getSchema() { //check schema override if ( schema != null ) { return schema; } SimpleFeatureType featureType = getAbsoluteSchema(); //this may be a view if ( query != null && query.getPropertyNames() != Query.ALL_NAMES) { synchronized ( this ) { if ( schema == null ) { schema = SimpleFeatureTypeBuilder.retype(featureType, query.getPropertyNames() ); } } return schema; } return featureType; } /** * Helper method for returning the underlying schema of the feature source. * This is a non-view this is the same as calling getSchema(), but in the * view case the underlying "true" schema is returned. */ protected final SimpleFeatureType getAbsoluteSchema() { //load the type from the state shared among feature sources ContentState state = entry.getState(transaction); SimpleFeatureType featureType = state.getFeatureType(); if (featureType == null) { //build and cache it synchronized (state) { if (featureType == null) { try { featureType = buildFeatureType(); } catch (IOException e) { throw new RuntimeException(e); } state.setFeatureType( featureType); } } } return featureType; } /** * Returns the bounds of the entire feature source. * <p> * This method delegates to {@link #getBounds(Query)}: * <pre> * <code>return getBounds(Query.ALL)</code>. * </pre> * </p> */ public final ReferencedEnvelope getBounds() throws IOException { //return all(entry.getState(transaction)).getBounds(); return getBounds(Query.ALL); } /** * Returns the bounds of the results of the specified query against the * feature source. * <p> * This method calls through to {@link #getBoundsInternal(Query)} which * subclasses must implement. It also contains optimizations which check * state for cached values. * </p> */ public final ReferencedEnvelope getBounds(Query query) throws IOException { query = joinQuery( query ); query = resolvePropertyNames(query); /* if ( query == Query.ALL ) { //check the cache //TODO: there should be a check for a view here if ( getState().getBounds() != null ) { return getState().getBounds(); } } */ // //calculate the bounds // ReferencedEnvelope bounds = getBoundsInternal(query); /* if ( query == Query.ALL ) { //update the cache synchronized (getState()) { getState().setBounds(bounds); } } */ return bounds; } // return filtered(entry.getState(transaction), query.getFilter()).getBounds(); //} /** * Calculates the bounds of a specified query. Subclasses must implement this * method. */ protected abstract ReferencedEnvelope getBoundsInternal(Query query) throws IOException; /** * Returns the count of the number of features of the feature source. * <p> * This method calls through to {@link #getCount(Query)} which * subclasses must implement. It also contains optimizations which check * state for cached values. * </p> */ public final int getCount(Query query) throws IOException { query = joinQuery( query ); query = resolvePropertyNames( query ); /* if ( query == Query.ALL ) { //check the cache if ( getState().getCount() != -1 ) { return getState().getCount(); } } */ //calculate the count //TODO: figure out if we need to calculate manually based on canFilter int count = getCountInternal( query ); /* if ( query == Query.ALL ) { //update the cache synchronized (getState()) { getState().setCount( count ); } } */ return count; } // return filtered(entry.getState(transaction), query.getFilter()).size(); //} /** * Calculates the number of features of a specified query. Subclasses must * implement this method. */ protected abstract int getCountInternal(Query query) throws IOException; /** * Returns the feature collection of all the features of the feature source. */ public final ContentFeatureCollection getFeatures() throws IOException { Query query = joinQuery(Query.ALL); return new ContentFeatureCollection( this, query ); //return getFeatures(Query.ALL); } /** * Returns a feature reader for all features. * <p> * This method calls through to {@link #getReader(Query)}. * </p> */ public final FeatureReader<SimpleFeatureType, SimpleFeature> getReader() throws IOException { return getReader(Query.ALL); } /** * Returns the feature collection if the features of the feature source which * meet the specified query criteria. */ public final ContentFeatureCollection getFeatures(Query query) throws IOException { query = joinQuery( query ); return new ContentFeatureCollection( this, query ); } /** * Returns a reader for the features specified by a query. * */ public final FeatureReader<SimpleFeatureType, SimpleFeature> getReader(Query query) throws IOException { query = joinQuery( query ); query = resolvePropertyNames(query); // see if we need to enable native sorting in order to support stable paging final int offset = query.getStartIndex() != null ? query.getStartIndex() : 0; if(offset > 0 & query.getSortBy() == null) { if(!getQueryCapabilities().supportsSorting(query.getSortBy())) throw new IOException("Feature source does not support this sorting " + "so there is no way a stable paging (offset/limit) can be performed"); DefaultQuery dq = new DefaultQuery(query); dq.setSortBy(new SortBy[] {SortBy.NATURAL_ORDER}); query = dq; } FeatureReader<SimpleFeatureType, SimpleFeature> reader = getReaderInternal( query ); // //apply wrappers based on subclass capabilities // //filtering if ( !canFilter() ) { if (query.getFilter() != null && query.getFilter() != Filter.INCLUDE ) { reader = new FilteringFeatureReader<SimpleFeatureType, SimpleFeature>( reader, query.getFilter() ); } } // reprojection if ( !canReproject() ) { if (query.getCoordinateSystemReproject() != null) { try { reader = new ReprojectFeatureReader(reader, query.getCoordinateSystemReproject()); } catch (Exception e) { if(e instanceof IOException) throw (IOException) e; else throw (IOException) new IOException("Error occurred trying to reproject data").initCause(e); } } } // offset if( !canOffset() && offset > 0 ) { // skip the first n records for(int i = 0; i < offset && reader.hasNext(); i++) { reader.next(); } } // max feature limit if ( !canLimit() ) { if (query.getMaxFeatures() != -1 && query.getMaxFeatures() < Integer.MAX_VALUE ) { reader = new MaxFeatureReader<SimpleFeatureType, SimpleFeature>(reader, query.getMaxFeatures()); } } //sorting if ( !canSort() ) { if ( query.getSortBy() != null && query.getSortBy().length != 0 ) { throw new UnsupportedOperationException( "sorting unsupported" ); } } //retyping if ( !canRetype() ) { if ( query.getPropertyNames() != Query.ALL_NAMES ) { //rebuild the type and wrap the reader SimpleFeatureType target = SimpleFeatureTypeBuilder.retype(getSchema(), query.getPropertyNames()); // do an equals check because we may have needlessly retyped (that is, // the subclass might be able to only partially retype) if ( !target.equals( reader.getFeatureType() ) ) { reader = new ReTypeFeatureReader( reader, target, false ); } } } return reader; } /** * Visit the features matching the provided query. * <p> * The default information will use getReader( query ) and pass each feature to the provided visitor. * Subclasses should override this method to optimise common visitors: * <ul> * <li> {@link Collection_AverageFunction} * <li> {@link Collection_BoundsFunction} * <li> (@link Collection_CountFunction} * <li> {@link Collection_MaxFunction} * <li> {@link Collection_MedianFunction} * <li> {@link Collection_MinFunction} * <li> {@link Collection_SumFunction} * <li> {@link Collection_UniqueFunction} * </ul> * Often in the case of Filter.INCLUDES the information can be determined from a file header or metadata table. * <p> * * @param visitor Visitor called for each feature * @param progress Used to report progress; and errors on a feature by feature basis * @throws IOException */ public void accepts( Query query, org.opengis.feature.FeatureVisitor visitor, org.opengis.util.ProgressListener progress) throws IOException { if( progress == null ) { progress = new NullProgressListener(); } if ( handleVisitor(query,visitor) ) { //all good, subclass handled return; } //subclass could not handle, resort to manually walkign through FeatureReader<SimpleFeatureType, SimpleFeature> reader = getReader(query); try{ float size = progress instanceof NullProgressListener ? 0.0f : (float) getCount( query ); float position = 0; progress.started(); while( reader.hasNext() ){ if (size > 0) progress.progress( position++/size ); try { SimpleFeature feature = reader.next(); visitor.visit(feature); } catch( Exception erp ){ progress.exceptionOccurred( erp ); } } } finally { progress.complete(); reader.close(); } } /** * Subclass method which allows subclasses to natively handle a visitor. * <p> * Subclasses would override this method and return true in cases where the specific * visitor could be handled without iterating over the entire result set of query. An * example would be handling visitors that calculate aggregate values. * </p> * @param query The query being made. * @param visitor The visitor to * * @return true if the visitor can be handled natively, otherwise false. */ protected boolean handleVisitor( Query query, FeatureVisitor visitor ) throws IOException { return false; } /** * Subclass method for returning a native reader from the datastore. * <p> * It is important to note that if the native reader intends to handle any * of the following natively: * <ul> * <li>reprojection</li> * <li>filtering</li> * <li>max feature limiting</li> * <li>sorting<li> * </ul> * Then it <b>*must*</b> set the corresonding flags to <code>true</code>: * <ul> * <li>{@link #canReproject()}</li> * <li>{@link #canFilter()}</li> * <li>{@link #canLimit()}</li> * <li>{@link #canSort()}<li> * </ul> * </p> * */ protected abstract FeatureReader<SimpleFeatureType, SimpleFeature> getReaderInternal( Query query ) throws IOException; /** * Determines if the datastore can natively perform reprojection.. * <p> * If the subclass can handle reprojection natively then it should override * this method to return <code>true</code>. In this case it <b>must</b> do * the reprojection or throw an exception. * </p> * <p> * Not overriding this method or returning <code>false</code> will case the * feature reader created by the subclass to be wrapped in a reprojecting * decorator when the query specifies a coordinate system reproject. * </p> * TODO: link to decorating feature reader */ protected boolean canReproject() { return false; } /** * Determines if the datastore can natively limit the number of features * returned in a query. * <p> * If the subclass can handle a map feature cap natively then it should override * this method to return <code>true</code>. In this case it <b>must</b> do * the cap or throw an exception. * </p> * <p> * Not overriding this method or returning <code>false</code> will case the * feature reader created by the subclass to be wrapped in a max feature capping * decorator when the query specifies a max feature cap. * </p> * @see MaxFeatureReader */ protected boolean canLimit() { return false; } /** * Determines if the datastore can natively skip the first <code>offset</code> number of features * returned in a query. * <p> * If the subclass can handle a map feature cap natively then it should override * this method to return <code>true</code>. In this case it <b>must</b> do * the cap or throw an exception. * </p> * <p> * Not overriding this method or returning <code>false</code> will case the * feature reader created by the subclass to be be accesset offset times before * being returned to the caller. * </p> */ protected boolean canOffset() { return false; } /** * Determines if the datastore can natively perform a filtering. * <p> * If the subclass can handle filtering natively it should override this method * to return <code>true</code>. In this case it <b>must</b> do the filtering * or throw an exception. This includes the case of partial native filtering * where the datastore can only handle part of the filter natively. In these * cases it is up to the subclass to apply a decorator to the reader it returns * which will handle any part of the filter can was not applied natively. See * {@link FilteringFeatureReader}. * </p> * <p> * Not overriding this method or returning <code>false</code> will cause the * feature reader created by the subclass to be wrapped in a filtering feature * reader when the query specifies a filter. See {@link FilteringFeatureReader}. * </p> */ protected boolean canFilter() { return false; } /** * Determines if the datasatore can natively perform "retyping" which includes * limiting the number of attributes returned and reordering of those attributes * <p> * If the subclass can handle retyping natively it should override this method * to return <code>true</code>. In this case it <b>must</b> do the retyping * or throw an exception. * </p> * <p> * Not overriding this method or returning <code>false</code> will cause the * feature reader created by the subclass to be wrapped in a retyping feature * reader when the query specifies a retype. * </p> * TODO: link to feature decorator */ protected boolean canRetype() { return false; } /** * Determines if the datastore can natively perform sorting. * <p> * If the subclass can handle retyping natively it should override this method * to return <code>true</code>. In this case it <b>must</b> do the retyping * or throw an exception. * </p> * <p> * Not overriding this method or returning <code>false</code> will cause an * exception to be thrown when the query specifies sorting. */ protected boolean canSort() { return false; } /** * Creates a new feature source for the specified query. * <p> * If the current feature source already has a defining query it is joined * to the specified query. * </p> * @param query * @return * @throws IOException */ public final ContentFeatureSource getView(Query query) throws IOException { query = joinQuery(query); query = resolvePropertyNames(query); //reflectively create subclass Class clazz = getClass(); try { Constructor c = clazz.getConstructor(ContentEntry.class,Query.class); ContentFeatureSource source = (ContentFeatureSource) c.newInstance(getEntry(),query); //set the transaction source.setTransaction( transaction ); return source; } catch( Exception e ) { String msg = "Subclass must implement Constructor(ContentEntry,Query)"; throw (IOException) new IOException( msg ).initCause(e); } } /** * Returns the feature collection for the features which match the specified * filter. * <p> * This method calls through to {@link #getFeatures(Query)}. * </p> */ public final ContentFeatureCollection getFeatures(Filter filter) throws IOException { return getFeatures( new DefaultQuery( getSchema().getTypeName(), filter ) ); } /** * Returns a reader for features specified by a particular filter. * <p> * This method calls through to {@link #getReader(Query)}. * </p> */ public final FeatureReader<SimpleFeatureType, SimpleFeature> getReader(Filter filter) throws IOException { return getReader( new DefaultQuery( getSchema().getTypeName(), filter )); } public final ContentFeatureSource getView(Filter filter) throws IOException { return getView( new DefaultQuery( getSchema().getTypeName(), filter ) ); } /** * Adds an listener or observer to the feature source. * <p> * Listeners are stored on a per-transaction basis. * </p> */ public final void addFeatureListener(FeatureListener listener) { entry.getState(transaction).addListener(listener); } /** * Removes a listener from the feature source. */ public final void removeFeatureListener(FeatureListener listener) { entry.getState(transaction).removeListener(listener); } /** * The hints provided by the feature store. * <p> * Subclasses should implement {@link #addHints(Set)} to provide additional * hints. * </p> * * @see FeatureSource#getSupportedHints() */ public final Set getSupportedHints() { return hints; } // // Internal API // /** * Subclass hook too add additional hints. * <p> * By default, the followings are already present: * <ul> * <li>{@link Hints#JTS_COORDINATE_SEQUENCE_FACTORY} * <li>{@link Hints#JTS_GEOMETRY_FACTORY} * </ul> * * </p> * @param hints The set of hints supported by the feature source. */ protected void addHints( Set<Hints.Key> hints ) { } /** * Convenience method for joining a query with the definining query of the * feature source. */ protected Query joinQuery( Query query ) { // if the defining query is unset or ALL just return the query passed in if ( this.query == null || this.query == Query.ALL ) { return query; } // if the query passed in is unset or ALL return the defining query if ( query == null ) { return this.query; } // join the queries return DataUtilities.mixQueries(this.query, query, null); } /** * This method changes the query object so that all propertyName references are resolved * to simple attribute names against the schema of the feature source. * <p> * For example, this method ensures that propertyName's such as "gml:name" are rewritten as * simply "name". *</p> */ protected Query resolvePropertyNames( Query query ) { Filter resolved = resolvePropertyNames( query.getFilter() ); if ( resolved == query.getFilter() ) { return query; } DefaultQuery newQuery = new DefaultQuery(query); newQuery.setFilter( resolved ); return newQuery; } /** Transform provided filter; resolving property names */ protected Filter resolvePropertyNames( Filter filter ) { if ( filter == null || filter == Filter.INCLUDE || filter == Filter.EXCLUDE ) { return filter; } return (Filter) filter.accept( new PropertyNameResolvingVisitor(getSchema()) , null); } /** * Creates the feature type or schema for the feature source. * <p> * Implementations should use {@link SimpleFeatureTypeBuilder} to build the * feature type. Also, the builder should be injected with the feature factory * which has been set on the datastore (see {@link ContentDataStore#getFeatureFactory()}. * Example: * <pre> * SimpleFeatureTypeBuilder b = new SimpleFeatureTypeBuilder(); * b.setFeatureTypeFactory( getDataStore().getFeatureTypeFactory() ); * * //build the feature type * ... * </pre> * </p> */ protected abstract SimpleFeatureType buildFeatureType() throws IOException; /** * Builds the query capabilities for this feature source. The default * implementation returns a newly built QueryCapabilities, subclasses * are advised to build their own. * @return * @throws IOException */ protected QueryCapabilities buildQueryCapabilities() { return new QueryCapabilities(); } /** * Returns a new feature collection containing all the features of the * feature source. * <p> * Subclasses are encouraged to provide a feature collection implementation * which provides optimized access to the underlying data format. * </p> * * @param state The state the feature collection must work from. */ //protected abstract ContentFeatureCollection all(ContentState state); /** * Returns a new feature collection containing all the features of the * feature source which match the specified filter. * <p> * Subclasses are encouraged to provide a feature collection implementation * which provides optimized access to the underlying data format. * </p> * @param state The state the feature collection must work from. * @param filter The constraint filtering the data to return. * */ //protected abstract ContentFeatureCollection filtered(ContentState state, Filter filter); /** * Returns a new feautre collection containing all the features of the * feature source sorted by a particular set of attributes. * <p> * This method accepts a filter optionally to filter returned content. This * parameter may be <code>null</code>, which which case no filtering should * occur. * </p> * @param state The state the feature collection must work from. * @param sort The sort criteria * @param filter A filter, possibly <code>null</code> * @return */ //protected abstract ContentFeatureCollection sorted(ContentState state, SortBy[] sort, Filter filter); /** * FeatureList representing sorted content. * <p> * Available via getFeatureSource(): * <ul> * <li>getFeatures().sort( sort ) * <li>getFeatures( filter ).sort( sort ) * <li>getFeatures( filter ).sort( sort ).sort( sort1 ); * </ul> * @param state * @param filter * @param order List<SortBy> used to determine sort order * @return subset of content */ //protected abstract FeatureList sorted(ContentState state, Filter filter, List order); /** * FeatureCollection<SimpleFeatureType, SimpleFeature> optimized for read-only access. * <p> * Available via getView( filter ): * <ul> * <li>getFeatures().sort( sort ) * <li>getFeatures( filter ).sort( sort ) * </ul> * <p> * In particular this method of data access is intended for rendering and other high speed * operations; care should be taken to optimize the use of FeatureVisitor. * <p> * @param state * @param filter * @return readonly access */ //protected abstract FeatureCollection<SimpleFeatureType, SimpleFeature> readonly(ContentState state, Filter filter); public QueryCapabilities getQueryCapabilities() { // lazy initialization, so that the subclass has all its data structures ready // when the method is called (it might need to consult them in order to decide // what query capabilities are really supported) if(queryCapabilities == null) { queryCapabilities = buildQueryCapabilities(); } return queryCapabilities; } }