/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.gss;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.opengis.wfs.DeleteElementType;
import net.opengis.wfs.InsertElementType;
import net.opengis.wfs.PropertyType;
import net.opengis.wfs.TransactionType;
import net.opengis.wfs.UpdateElementType;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.config.ConfigurationListenerAdapter;
import org.geoserver.config.GeoServer;
import org.geoserver.config.ServiceInfo;
import org.geoserver.gss.GSSInfo.GSSMode;
import org.geotools.data.DataAccess;
import org.geotools.data.DataUtilities;
import org.geotools.data.FeatureStore;
import org.geotools.data.Transaction;
import org.geotools.data.VersioningDataStore;
import org.geotools.data.jdbc.JDBCUtils;
import org.geotools.data.postgis.VersionedPostgisDataStore;
import org.geotools.feature.FeatureIterator;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
/**
* Provides core services used by both Central and Unit behaviors
*
* @author Andrea Aime - OpenGeo
*/
public class GSSCore {
// metadata tables and sql to build them
static final String SYNCH_TABLES = "synch_tables";
static final String SYNC_TABLES_CREATION = "CREATE TABLE synch_tables(\n"
+ "table_id SERIAL PRIMARY KEY, \n" //
+ "table_name VARCHAR(256) NOT NULL, \n" //
+ "type CHAR(1) NOT NULL CHECK (type in ('p', 'b', '2')))";
static final String SYNCH_HISTORY = "synch_history";
static final String SYNCH_HISTORY_CREATION = "CREATE TABLE synch_history(\n"
+ "id SERIAL PRIMARY KEY,\n" //
+ "table_name VARCHAR(256) NOT NULL,\n" //
+ "local_revision BIGINT NOT NULL,\n" //
+ "central_revision BIGINT,\n" //
+ "unique(table_name, local_revision, central_revision))";
static final String SYNCH_CONFLICTS = "synch_conflicts";
// conflict can be in 'c', conflict, 'r', resolved or 'm', clean merge state
// clean merge is a marker stating that the same change occurred both locally
// and in central, and as such it should not be reported in GetDiff
static final String SYNCH_CONFLICTS_CREATION = "CREATE TABLE synch_conflicts(\n"
+ "id SERIAL PRIMARY KEY,\n" + "table_name VARCHAR(256) NOT NULL,\n" //
+ "feature_id UUID NOT NULL,\n" //
+ "local_revision BIGINT NOT NULL,\n" //
+ "date_created TIMESTAMP NOT NULL,\n" //
+ "state CHAR(1) NOT NULL CHECK (state in ('c', 'r', 'm')),\n" //
+ "date_resolved TIMESTAMP,\n" //
+ "local_feature TEXT,\n" //
+ "unique(table_name, feature_id, local_revision))";
static final String SYNCH_UNITS = "synch_units";
static final String SYNCH_UNITS_CREATION = "CREATE TABLE synch_units (\n" //
+ " unit_id SERIAL PRIMARY KEY,\n" //
+ " unit_name VARCHAR(1024) NOT NULL,\n" //
+ " unit_address VARCHAR(2048) NOT NULL,\n" //
+ " synch_user VARCHAR(256),\n" //
+ " synch_password VARCHAR(256),\n" //
+ " time_start TIME,\n" //
+ " time_end TIME,\n" //
+ " synch_interval REAL,\n" //
+ " synch_retry REAL,\n" //
+ " errors BOOLEAN\n" //
+ ");\n" //
+ "select AddGeometryColumn('synch_units','geom',4326,'GEOMETRY',2)";
static final String SYNCH_UNIT_TABLES = "synch_unit_tables";
static final String SYNCH_UNIT_TABLES_CREATION = //
"CREATE TABLE synch_unit_tables (\n" + //
" id SERIAL PRIMARY KEY," + //
" unit_id INTEGER NOT NULL REFERENCES synch_units(unit_id),\n" + //
" table_id INTEGER NOT NULL REFERENCES synch_tables(table_id),\n" + //
" last_synchronization TIMESTAMP,\n" + //
" last_failure TIMESTAMP,\n" + //
" getdiff_central_revision BIGINT,\n" + //
" last_unit_revision BIGINT,\n" + //
" unique (unit_id, table_id)\n" + //
")";
static final String SYNCH_OUTSTANDING = "synch_outstanding";
static final String SYNCH_OUTSTANDING_CREATION = //
"CREATE VIEW synch_outstanding \n"
+ "AS SELECT synch_tables.*, \n"
+ " synch_units.*, \n"
+ " synch_unit_tables.last_synchronization,\n"
+ " synch_unit_tables.last_failure, \n"
+ " synch_unit_tables.getdiff_central_revision, \n"
+ " synch_unit_tables.last_unit_revision\n"
+ "FROM (synch_units inner join synch_unit_tables \n"
+ " on synch_units.unit_id = synch_unit_tables.unit_id)\n"
+ " inner join synch_tables \n"
+ " on synch_tables.table_id = synch_unit_tables.table_id\n"
+ "WHERE ((time_start < LOCALTIME AND LOCALTIME < time_end) \n"
+ " OR (time_start IS NULL) OR (time_end IS NULL))\n"
+ " AND ((now() - last_synchronization > synch_interval * interval '1 minute') \n"
+ " OR last_synchronization IS NULL)\n"
+ " AND (last_failure is null OR"
+ " now() - last_failure > synch_retry * interval '1 minute');\n"
+ "INSERT INTO geometry_columns VALUES('', 'public', 'synch_outstanding', 'geom', 2, 4326, 'GEOMETRY')";
GeoServer geoServer;
public GSSCore(GeoServer geoServer) {
this.geoServer = geoServer;
// try to version enable the tables that need to
versionEnableTables();
// add a listener that will try to version enable tables on config changes
geoServer.addListener(new ConfigurationListenerAdapter() {
@Override
public void handlePostServiceChange(ServiceInfo service) {
if (service instanceof GSSInfo) {
versionEnableTables();
}
}
@Override
public void handlePostGlobalChange(org.geoserver.config.GeoServerInfo global) {
versionEnableTables();
}
});
}
void versionEnableTables() {
try {
ensureEnabled();
} catch (Exception e) {
// nothing to do really, the service might not be configured enough
}
}
GSSInfo getServiceInfo() {
return geoServer.getService(GSSInfo.class);
}
/**
* Checks the module is ready to be used. TODO: move this to a listener so that we don't do all
* the ckecks for every request
*/
void ensureEnabled() {
GSSInfo info = getServiceInfo();
// basic sanity checks on the config
if (info == null) {
throw new GSSException("The service is not properly configured, gssInfo not found");
}
if (info.getMode() == null) {
throw new GSSException("The gss mode has not been configured");
}
if (info.getVersioningDataStore() == null || !info.getVersioningDataStore().isEnabled()) {
throw new GSSException("The service is disabled as the "
+ "versioning datastore is not available/disabled");
}
FeatureIterator<SimpleFeature> fi = null;
try {
// basic sanity checks on the datastore
DataAccess ds = info.getVersioningDataStore().getDataStore(null);
if (!(ds instanceof VersionedPostgisDataStore)) {
throw new GSSException(
"The store attached to the gss module is not a PostGIS versioning one");
}
VersionedPostgisDataStore dataStore = (VersionedPostgisDataStore) ds;
Set<String> typeNames = new HashSet<String>(Arrays.asList(dataStore.getTypeNames()));
// the synchronized tables list
if (!typeNames.contains(SYNCH_TABLES)) {
runStatement(dataStore, SYNC_TABLES_CREATION);
}
dataStore.setVersioned(SYNCH_TABLES, false, null, null);
// version enable all tables that are supposed to be shared
fi = dataStore.getFeatureSource(SYNCH_TABLES).getFeatures().features();
while (fi.hasNext()) {
String tableName = (String) fi.next().getAttribute("table_name");
dataStore.setVersioned(tableName, true, null, null);
}
fi.close();
if (info.getMode() == GSSMode.Unit) {
// hmm... we should really try to use createSchema() instead, but atm
// we don't have the necessary control over it
// the unit synchronisation history
if (!typeNames.contains(SYNCH_HISTORY)) {
runStatement(dataStore, SYNCH_HISTORY_CREATION);
}
dataStore.setVersioned(SYNCH_HISTORY, false, null, null);
// the conflict table
if (!typeNames.contains(SYNCH_CONFLICTS)) {
runStatement(dataStore, SYNCH_CONFLICTS_CREATION);
}
dataStore.setVersioned(SYNCH_CONFLICTS, true, null, null);
} else {
if (!typeNames.contains(SYNCH_UNITS)) {
runStatement(dataStore, SYNCH_UNITS_CREATION);
}
dataStore.setVersioned(SYNCH_UNITS, false, null, null);
if (!typeNames.contains(SYNCH_UNIT_TABLES)) {
runStatement(dataStore, SYNCH_UNIT_TABLES_CREATION);
}
dataStore.setVersioned(SYNCH_UNITS, false, null, null);
if (!typeNames.contains(SYNCH_OUTSTANDING)) {
runStatement(dataStore, SYNCH_OUTSTANDING_CREATION);
}
}
} catch (Exception e) {
throw new GSSException("A problem occurred while checking the versioning store", e);
} finally {
if (fi != null) {
fi.close();
}
}
}
void runStatement(VersionedPostgisDataStore dataStore, String sqlStatement) throws IOException,
SQLException {
Connection conn = null;
Statement st = null;
try {
conn = dataStore.getConnection(Transaction.AUTO_COMMIT);
st = conn.createStatement();
st.execute(sqlStatement);
} finally {
JDBCUtils.close(st);
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, null);
}
}
public void ensureUnitEnabled() {
ensureEnabled();
if (getServiceInfo().getMode() != GSSMode.Unit) {
throw new GSSException("gss configured in Central mode, won't do Unit service calls");
}
}
public void ensureCentralEnabled() {
ensureEnabled();
if (getServiceInfo().getMode() != GSSMode.Central) {
throw new GSSException("gss configured in Unit mode, won't do synchronisation services");
}
}
/**
* Finds the versioning datastore configured for this service
*
* @return
* @throws IOException
*/
public VersioningDataStore getVersioningStore() throws IOException {
return (VersioningDataStore) getServiceInfo().getVersioningDataStore().getDataStore(null);
}
/**
* Returns the datastore configuration for this service
*
* @return
* @throws IOException
*/
public DataStoreInfo getVersioningStoreInfo() {
return getServiceInfo().getVersioningDataStore();
}
/**
* Returns the operation mode
*
* @return
*/
public GSSMode getMode() {
return getServiceInfo().getMode();
}
/**
* Applies the specified transaction to the provided feature store
*
* @param changes
* @param store
*/
public void applyChanges(TransactionType changes,
FeatureStore<SimpleFeatureType, SimpleFeature> store) throws IOException {
if (changes == null)
return;
List<DeleteElementType> deletes = changes.getDelete();
List<UpdateElementType> updates = changes.getUpdate();
List<InsertElementType> inserts = changes.getInsert();
for (DeleteElementType delete : deletes) {
store.removeFeatures(delete.getFilter());
}
for (UpdateElementType update : updates) {
List<PropertyType> props = update.getProperty();
List<AttributeDescriptor> atts = new ArrayList<AttributeDescriptor>(props.size());
List<Object> values = new ArrayList<Object>(props.size());
for (PropertyType prop : props) {
atts.add(store.getSchema().getDescriptor(prop.getName().getLocalPart()));
values.add(prop.getValue());
}
AttributeDescriptor[] attArray = (AttributeDescriptor[]) atts
.toArray(new AttributeDescriptor[atts.size()]);
Object[] valArray = (Object[]) values.toArray(new Object[values.size()]);
store.modifyFeatures(attArray, valArray, update.getFilter());
}
for (InsertElementType insert : inserts) {
List<SimpleFeature> features = insert.getFeature();
store.addFeatures(DataUtilities.collection(features));
}
}
/**
* Returns the number of changes contained in the transaction
*
* @param changes
* @return
*/
public int countChanges(TransactionType changes) {
if (changes == null) {
return 0;
}
int count = 0;
count += changes.getDelete().size();
count += changes.getUpdate().size();
count += changes.getInsert().size();
return count;
}
public static void main(String[] args) {
System.out.println("Unit tables creation");
System.out.println("--------------------");
System.out.println();
System.out.println(SYNC_TABLES_CREATION);
System.out.println(SYNCH_HISTORY_CREATION);
System.out.println(SYNCH_CONFLICTS_CREATION);
System.out.println();
System.out.println();
System.out.println();
System.out.println("Central tables creation");
System.out.println("--------------------");
System.out.println();
System.out.println(SYNCH_UNITS_CREATION);
System.out.println(SYNCH_UNIT_TABLES_CREATION);
System.out.println(SYNCH_OUTSTANDING_CREATION);
}
}