/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2006-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.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import org.geotools.data.BatchFeatureEvent;
import org.geotools.data.FeatureEvent;
import org.geotools.data.FeatureListener;
import org.geotools.data.FeatureSource;
import org.geotools.data.Transaction;
import org.geotools.data.FeatureEvent.Type;
import org.geotools.data.Transaction.State;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.identity.FeatureId;
/**
* The state of an entry in a datastore, maintained on a per-transaction basis.
* <p>
* State is maintained on a per transaction basis (see {@link ContentEntry}}.
* State maintained includes cached values such as:
* <ul>
* <li>feature type ({@link #getFeatureType()}
* <li>number of features ({@link #getCount()}
* <li>spatial extent ({@link #getBounds()}.
* </ul>
* Other types of state depend on the data format. For instance, a jdbc database
* backed format would probably want to store a database connection as state.
*</p>
* <p>
* This class is a "data object" and is not thread safe. It is up to clients of
* this class to ensure that values are set in a thread-safe / synchronized
* manner. For example:
* <pre>
* <code>
* ContentState state = ...;
*
* //get the count
* int count = state.getCount();
* if ( count == -1 ) {
* synchronized ( state ) {
* count = calculateCount();
* state.setCount( count );
* }
* }
* </code>
* </pre>
* </p>
* <p>
* This class may be extended. Subclasses may extend (not override) the
* following methods:
* <ul>
* <li>{@link #flush()}
* <li>{@link #close()}
* </ul>
* Subclasses should also override {@link #copy()}.
* </p>
*
* @author Jody Garnett, Refractions Research Inc.
* @author Justin Deoliveira, The Open Planning Project
*
* @source $URL$
*/
public class ContentState {
/**
* Transaction the state works from.
*/
protected Transaction tx;
/**
* cached feature type
*/
protected SimpleFeatureType featureType;
/**
* cached number of features
*/
protected int count = -1;
/**
* cached bounds of features
*/
protected ReferencedEnvelope bounds;
/**
* entry maintaining the state
*/
protected ContentEntry entry;
/**
* Even used for batch notification; used to collect the bounds and feature ids generated
* over the course of a transaction.
*/
protected BatchFeatureEvent batchFeatureEvent;
/**
* observers
*/
protected List<FeatureListener> listeners = Collections.synchronizedList(new ArrayList<FeatureListener>());
/**
* Callback used to issue batch feature events when commit/rollback issued
* on the transaction.
*/
protected State callback = new State(){
public void setTransaction(Transaction transaction) {
}
public void addAuthorization(String AuthID) throws IOException {
}
public void commit() throws IOException {
fireBatchFeatureEvent(true);
}
public void rollback() throws IOException {
fireBatchFeatureEvent(false);
}
};
/**
* Creates a new state.
*
* @param entry The entry for the state.
*/
public ContentState(ContentEntry entry) {
this.entry = entry;
}
/**
* Creates a new state from a previous one.
* <p>
* All state from the specified <tt>state</tt> is copied. Therefore subclasses
* extending this constructor should clone all mutable objects.
* </p>
*
* @param state The existing state.
*/
protected ContentState(ContentState state) {
this( state.getEntry() );
featureType = state.featureType;
count = state.count;
bounds = state.bounds == null ?
null : new ReferencedEnvelope( state.bounds );
batchFeatureEvent = null;
}
/**
* The entry which maintains the state.
*/
public ContentEntry getEntry() {
return entry;
}
/**
* The transaction associated with the state.
*/
public Transaction getTransaction() {
return tx;
}
/**
* Sets the transaction associated with the state.
*/
public void setTransaction(Transaction tx) {
this.tx = tx;
if( tx != Transaction.AUTO_COMMIT ){
tx.putState( this.entry, callback );
}
}
/**
* The cached feature type.
*/
public final SimpleFeatureType getFeatureType(){
return featureType;
}
/**
* Sets the cached feature type.
*/
public final void setFeatureType( SimpleFeatureType featureType ){
this.featureType = featureType;
}
/**
* The cached number of features.
*
*/
public final int getCount(){
return count;
}
/**
* Sets the cached number of features.
*/
public final void setCount(int count){
this.count = count;
}
/**
* The cached spatial extent.
*/
public final ReferencedEnvelope getBounds(){
return bounds;
}
/**
* Sets the cached spatial extent.
*/
public final void setBounds( ReferencedEnvelope bounds ){
this.bounds = bounds;
}
/**
* Adds a listener for collection events.
*
* @param listener The listener to add
*/
public final void addListener(FeatureListener listener) {
listeners.add(listener);
}
/**
* Removes a listener for collection events.
*
* @param listener The listener to remove
*/
public final void removeListener(FeatureListener listener) {
listeners.remove(listener);
}
public BatchFeatureEvent getBatchFeatureEvent() {
return batchFeatureEvent;
}
/**
* Used to quickly test if any listeners are available.
* @return
*/
public final boolean hasListener(){
if( !listeners.isEmpty() ){
return true;
}
if( this.tx == Transaction.AUTO_COMMIT && this.entry.state.size() > 1){
// We are the auto commit state; and there is at least one other thread to notify
return true;
}
return false;
}
public void fireFeatureUpdated(FeatureSource<?,?> source, Feature feature,
ReferencedEnvelope before) {
if( listeners.isEmpty() && tx != Transaction.AUTO_COMMIT) return;
Filter filter = idFilter(feature);
ReferencedEnvelope bounds = new ReferencedEnvelope( feature.getBounds() );
bounds.expandToInclude( before );
FeatureEvent event = new FeatureEvent(source, Type.CHANGED, bounds, filter );
fireFeatureEvent( event );
}
/**
* Used to issue a Type.ADDED FeatureEvent indicating a new feature being created
* @param source
* @param feature
*/
public final void fireFeatureAdded( FeatureSource<?,?> source, Feature feature ){
if( listeners.isEmpty() && tx != Transaction.AUTO_COMMIT) return;
Filter filter = idFilter(feature);
ReferencedEnvelope bounds = new ReferencedEnvelope( feature.getBounds() );
FeatureEvent event = new FeatureEvent(source, Type.ADDED, bounds, filter );
fireFeatureEvent( event );
}
public void fireFeatureRemoved(FeatureSource<?,?> source, Feature feature) {
if( listeners.isEmpty() && tx != Transaction.AUTO_COMMIT) return;
Filter filter = idFilter(feature);
ReferencedEnvelope bounds = new ReferencedEnvelope( feature.getBounds() );
FeatureEvent event = new FeatureEvent(source, Type.REMOVED, bounds, filter );
fireFeatureEvent( event );
}
/**
* Helper method or building fid filters.
*/
Filter idFilter( Feature feature ) {
FilterFactory ff = this.entry.dataStore.getFilterFactory();
Set<FeatureId> fids = new HashSet<FeatureId>();
fids.add( feature.getIdentifier() );
return ff.id( fids );
}
/**
* Used to issue a single FeatureEvent.
* <p>
* If this content state is used for Transaction.AUTO_COMMIT the notification
* will be passed to all interested parties.
* <p>
* If not this event will be recored as part of a BatchFeatureEvent that will
* to be issued using issueBatchFeatureEvent()
* @param event
*/
public final void fireFeatureEvent( FeatureEvent event){
if( this.tx == Transaction.AUTO_COMMIT ){
this.entry.notifiyFeatureEvent(this, event );
}
else {
// we are not in auto-commit mode so we need to batch
// up the changes for when the commit goes out
if( batchFeatureEvent == null ){
batchFeatureEvent = new BatchFeatureEvent( event.getFeatureSource() );
}
batchFeatureEvent.add( event );
}
if( listeners.isEmpty() ){
return;
}
for (FeatureListener listener : listeners) {
try {
listener.changed( event );
}
catch (Throwable t ){
this.entry.dataStore.LOGGER.log( Level.WARNING, "Problem issuing batch feature event "+event, t );
}
}
}
/**
* Notifies all waiting listeners that a commit has been issued;
* this notification is also sent to our
*/
public final void fireBatchFeatureEvent(boolean isCommit){
if( batchFeatureEvent == null ){
return;
}
if( listeners.isEmpty() ){
return;
}
if( isCommit ){
batchFeatureEvent.setType( Type.COMMIT );
}
else {
batchFeatureEvent.setType( Type.ROLLBACK );
}
for (FeatureListener listener : listeners) {
try {
listener.changed( batchFeatureEvent );
}
catch (Throwable t ){
this.entry.dataStore.LOGGER.log( Level.WARNING, "Problem issuing batch feature event "+batchFeatureEvent, t );
}
}
// Let others know a modifications was made
this.entry.notifiyFeatureEvent(this, batchFeatureEvent );
batchFeatureEvent = null;
}
/**
* Clears cached state.
* <p>
* This method does not affect any non-cached state. This method may be
* extended by subclasses, but not overiden.
* </p>
*/
public void flush() {
featureType = null;
count = -1;
bounds = null;
}
/**
* Clears all state.
* <p>
* Any resources that the state holds onto (like a database connection) should
* be closed or disposes when this method is called. This method may be
* extended by subclasses, but not overiden.
* </p>
*/
public void close() {
featureType = null;
if( listeners != null ){
listeners.clear();
listeners = null;
}
}
/**
* Copies the state.
* <p>
* Subclasses shold override this method. Any mutable state objects should
* be cloned.
* </p>
*
* @return A copy of the state.
*/
public ContentState copy() {
return new ContentState( this );
}
}