/* * 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.postgis; import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.logging.Logger; import org.geotools.data.DataSourceException; import org.geotools.data.FeatureWriter; import org.geotools.data.Transaction; import org.geotools.data.VersioningDataStore; import org.geotools.data.jdbc.JDBCTransactionState; import org.geotools.data.jdbc.JDBCUtils; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.IllegalAttributeException; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; /** * JDBC Transaction state that holds current revision, modified bounding box and the list of dirty * feature types. On commit, these are update on the db. * * @author aaime * @since 2.4 * */ class VersionedJdbcTransactionState extends JDBCTransactionState { /** The logger for the postgis module. */ protected static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.data.postgis"); private long revision; private ReferencedEnvelope bbox; private HashSet dirtyTypes; private HashMap dirtyFids; private WrappedPostgisDataStore wrapped; private Transaction transaction; private static final double EPS = 0.000001; public VersionedJdbcTransactionState(Connection connection, WrappedPostgisDataStore wrapped) throws IOException { super(connection); this.wrapped = wrapped; reset(); } /** * Resets this state so that a new revision information is ready to be built */ private void reset() { this.revision = Long.MIN_VALUE; this.bbox = new ReferencedEnvelope(DefaultGeographicCRS.WGS84); this.dirtyTypes = new HashSet(); this.dirtyFids = new HashMap(); } /** * Returns the revision currently created during the transaction, eventually creating the * changesets record if not available * * @throws IOException */ public long getRevision() throws IOException { if (revision == Long.MIN_VALUE) { revision = writeRevision(transaction, bbox); transaction.putProperty(VersionedPostgisDataStore.REVISION, new Long(revision)); transaction.putProperty(VersionedPostgisDataStore.VERSION, String.valueOf(revision)); } return revision; } /** * Marks the specified type name as dirty, modified during the transaction * * @param typeName */ public void setTypeNameDirty(String typeName) { dirtyTypes.add(typeName); } /** * Expands the current lat/lon dirty area * * @param envelope * a new dirtied area, expressed in EPSG:4326 crs */ public void expandDirtyBounds(Envelope envelope) { bbox.expandToInclude(envelope); } /** * Marks a specified FID as dirty. This is used to avoid to do versioned operations * on the same feature multiple times in the same transaction. The first must create * the new versions, the others should operate against the new record * @param ft * @param fid */ public void setFidDirty(String typeName, String fid) { getCreateDirtyFids(typeName).add(fid); } /** * Marks a set of FIDs as dirty. This is used to avoid to do versioned operations * on the same feature multiple times in the same transaction. The first must create * the new versions, the others should operate against the new record * @param ft * @param fid */ public void setFidsDirty(String typeName, Collection<String> fids) { getCreateDirtyFids(typeName).addAll(fids); } /** * Returns (and eventually builds) the dirty fid set for the specified type name */ Set<String> getCreateDirtyFids(String typeName) { Set fids = (Set) dirtyFids.get(typeName); if(fids == null) { fids = new HashSet(); dirtyFids.put(typeName, fids); } return fids; } /** * Returns true if a specific feature has already been modified during this transaction * @param typeName * @param fid * @return */ public boolean isFidDirty(String typeName, String fid) { Set fids = (Set) dirtyFids.get(typeName); if(fids == null) return false; return fids.contains(fid); } public void setTransaction(Transaction transaction) { super.setTransaction(transaction); this.transaction = transaction; if (transaction == null) { // setup for fail fast if anyone tries to keep using this state // object // afer the transaction has been closed bbox = null; dirtyTypes = null; } } public void commit() throws IOException { // first, check we touched at least one versioned table if (!dirtyTypes.isEmpty()) { // grab author and message, they might have been updated since revsion insertion String author = (String) transaction.getProperty(VersioningDataStore.AUTHOR); String message = (String) transaction.getProperty(VersioningDataStore.MESSAGE); // first write down modified envelope SimpleFeature f = null; FeatureWriter<SimpleFeatureType, SimpleFeature> writer = null; try { // build filter to extract the appropriate changeset record FilterFactory ff = CommonFactoryFinder.getFilterFactory(null); Filter revisionFilter = ff.id(Collections.singleton(ff.featureId(VersionedPostgisDataStore.TBL_CHANGESETS + "." + getRevision()))); // get a writer for the changeset record we want to update writer = wrapped.getFeatureWriter(VersionedPostgisDataStore.TBL_CHANGESETS, (org.geotools.filter.Filter) revisionFilter, transaction); if (!writer.hasNext()) { // who ate my changeset record ?!? throw new IOException("Could not find the changeset record " + "that should have been set in the versioned datastore on " + "versioned jdbc state creation"); } // update it f = writer.next(); f.setAttribute("author", author); f.setAttribute("message", message); f.setDefaultGeometry(toLatLonRectange(bbox)); writer.write(); } catch (IllegalAttributeException e) { // if this happens there's a programming error throw new DataSourceException("Could not set an attribute in changesets, " + "most probably the table schema has been tampered with.", e); } finally { if (writer != null) writer.close(); } // then write down the modified feature types Statement st = null; try { st = getConnection().createStatement(); for (Iterator it = dirtyTypes.iterator(); it.hasNext();) { String typeName = (String) it.next(); execute(st, "INSERT INTO " + VersionedPostgisDataStore.TBL_TABLESCHANGED + " " + "SELECT " + revision + ", id " + "FROM " + VersionedPostgisDataStore.TBL_VERSIONEDTABLES + " WHERE SCHEMA = '" + wrapped.getConfig().getDatabaseSchemaName() + "' " + "AND NAME = '" + typeName + "'"); } } catch (SQLException e) { throw new DataSourceException( "Error occurred while trying to save modified tables for " + "this changeset. This should not happen, probaly there's a " + "bug at work here.", e); } finally { JDBCUtils.close(st); } } // aah, all right, now we can really commit this transaction and be happy super.commit(); // reset revision, we create a new revision for each new commit reset(); } public boolean isRevisionSet() { return revision == Long.MIN_VALUE; } /** * Takes a referenced envelope and turns it into a lat/lon Polygon * * @param envelope * @return * @throws TransformException */ Geometry toLatLonRectange(final ReferencedEnvelope env) throws IOException { ReferencedEnvelope envelope = new ReferencedEnvelope(env); try { // since we cannot work with a null geometry in commits to // changesets, let's return a very small envelope... // an empty envelope gets turned into a point if (envelope == null || envelope.isEmpty()) { envelope = new ReferencedEnvelope(new Envelope(0, EPS , 0, EPS), DefaultGeographicCRS.WGS84); } else { envelope = envelope.transform(DefaultGeographicCRS.WGS84, true); if(envelope.getHeight() == 0.0 || envelope.getWidth() == 0.0) envelope.expandBy(EPS); } GeometryFactory gf = new GeometryFactory(); return gf.toGeometry(envelope); } catch (Exception e) { throw new DataSourceException("An error occurred while trying to builds a " + "lat/lon polygon equivalent to " + envelope, e); } } /** * Stores a commit message in the CHANGESETS table and return the associated revision number. * * @param conn * @return * @throws IOException */ protected long writeRevision(Transaction t, ReferencedEnvelope bbox) throws IOException { SimpleFeature f = null; FeatureWriter<SimpleFeatureType, SimpleFeature> writer = null; String author = (String) t.getProperty(VersioningDataStore.AUTHOR); String message = (String) t.getProperty(VersioningDataStore.MESSAGE); Statement st = null; try { // we need to make sure that revision N+1 is committed after N is committed, otherwise // the history will be ruined st = getConnection().createStatement(); st.execute("LOCK TABLE " + VersionedPostgisDataStore.TBL_CHANGESETS + " IN EXCLUSIVE MODE"); writer = wrapped.getFeatureWriterAppend(VersionedPostgisDataStore.TBL_CHANGESETS, t); f = writer.next(); f.setAttribute("author", author); f.setAttribute("message", message); f.setAttribute("date", new Date()); f.setDefaultGeometry(toLatLonRectange(bbox)); writer.write(); } catch (IllegalAttributeException e) { // if this happens there's a programming error throw new IOException("Could not set an attribute in changesets, " + "most probably the table schema has been tampered with."); } catch (SQLException e) { throw new DataSourceException("Could not set a lock on the table changesets", e); } finally { if(st != null) JDBCUtils.close(st); if(writer != null) writer.close(); } return ((Long) f.getAttribute("revision")).longValue(); } /** * Logs the sql at info level, then executes the command * * @param st * @param sql * @throws SQLException */ protected void execute(Statement st, String sql) throws SQLException { LOGGER.fine(sql); st.execute(sql); } /** * Returns the transaction associated to this state * * @return */ Transaction getTransaction() { return transaction; } }