001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2002-2006, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.data.postgis;
017:
018: import java.io.IOException;
019: import java.sql.Connection;
020: import java.sql.SQLException;
021: import java.sql.Statement;
022: import java.util.Collections;
023: import java.util.Date;
024: import java.util.HashMap;
025: import java.util.HashSet;
026: import java.util.Iterator;
027: import java.util.Set;
028: import java.util.logging.Logger;
029:
030: import org.geotools.data.DataSourceException;
031: import org.geotools.data.FeatureWriter;
032: import org.geotools.data.Transaction;
033: import org.geotools.data.jdbc.JDBCTransactionState;
034: import org.geotools.data.jdbc.JDBCUtils;
035: import org.geotools.factory.CommonFactoryFinder;
036: import org.geotools.feature.Feature;
037: import org.geotools.feature.FeatureType;
038: import org.geotools.feature.IllegalAttributeException;
039: import org.geotools.geometry.jts.ReferencedEnvelope;
040: import org.geotools.referencing.crs.DefaultGeographicCRS;
041: import org.opengis.filter.Filter;
042: import org.opengis.filter.FilterFactory;
043: import org.opengis.referencing.operation.TransformException;
044:
045: import com.vividsolutions.jts.geom.Envelope;
046: import com.vividsolutions.jts.geom.Geometry;
047: import com.vividsolutions.jts.geom.GeometryFactory;
048:
049: /**
050: * JDBC Transaction state that holds current revision, modified bounding box and the list of dirty
051: * feature types. On commit, these are update on the db.
052: *
053: * @author aaime
054: * @since 2.4
055: *
056: */
057: class VersionedJdbcTransactionState extends JDBCTransactionState {
058:
059: /** The logger for the postgis module. */
060: protected static final Logger LOGGER = org.geotools.util.logging.Logging
061: .getLogger("org.geotools.data.postgis");
062:
063: private long revision;
064:
065: private ReferencedEnvelope bbox;
066:
067: private HashSet dirtyTypes;
068:
069: private HashMap dirtyFids;
070:
071: private WrappedPostgisDataStore wrapped;
072:
073: private Transaction transaction;
074:
075: private static final double EPS = 0.000001;
076:
077: public VersionedJdbcTransactionState(Connection connection,
078: WrappedPostgisDataStore wrapped) throws IOException {
079: super (connection);
080: this .wrapped = wrapped;
081: reset();
082: }
083:
084: /**
085: * Resets this state so that a new revision information is ready to be built
086: */
087: private void reset() {
088: this .revision = Long.MIN_VALUE;
089: this .bbox = new ReferencedEnvelope(DefaultGeographicCRS.WGS84);
090: this .dirtyTypes = new HashSet();
091: this .dirtyFids = new HashMap();
092: }
093:
094: /**
095: * Returns the revision currently created during the transaction, eventually creating the
096: * changesets record if not available
097: *
098: * @throws IOException
099: */
100: public long getRevision() throws IOException {
101: if (revision == Long.MIN_VALUE) {
102: revision = writeRevision(transaction, bbox);
103: transaction.putProperty(VersionedPostgisDataStore.REVISION,
104: new Long(revision));
105: transaction.putProperty(VersionedPostgisDataStore.VERSION,
106: String.valueOf(revision));
107: }
108: return revision;
109: }
110:
111: /**
112: * Marks the specified type name as dirty, modified during the transaction
113: *
114: * @param typeName
115: */
116: public void setTypeNameDirty(String typeName) {
117: dirtyTypes.add(typeName);
118: }
119:
120: /**
121: * Expands the current lat/lon dirty area
122: *
123: * @param envelope
124: * a new dirtied area, expressed in EPSG:4326 crs
125: */
126: public void expandDirtyBounds(Envelope envelope) {
127: bbox.expandToInclude(envelope);
128: }
129:
130: /**
131: * Marks a specified FID as dirty. This is used to avoid to do versioned operations
132: * on the same feature multiple times in the same transaction. The first must create
133: * the new versions, the others should operate against the new record
134: * @param ft
135: * @param fid
136: */
137: public void setFidDirty(String typeName, String fid) {
138: Set fids = (Set) dirtyFids.get(typeName);
139: if (fids == null) {
140: fids = new HashSet();
141: dirtyFids.put(typeName, fids);
142: }
143: fids.add(fid);
144: }
145:
146: /**
147: * Returns true if a specific feature has already been modified during this transaction
148: * @param typeName
149: * @param fid
150: * @return
151: */
152: public boolean isFidDirty(String typeName, String fid) {
153: Set fids = (Set) dirtyFids.get(typeName);
154: if (fids == null)
155: return false;
156: return fids.contains(fid);
157: }
158:
159: public void setTransaction(Transaction transaction) {
160: super .setTransaction(transaction);
161: this .transaction = transaction;
162: if (transaction == null) {
163: // setup for fail fast if anyone tries to keep using this state
164: // object
165: // afer the transaction has been closed
166: bbox = null;
167: dirtyTypes = null;
168: }
169: }
170:
171: public void commit() throws IOException {
172: // first, check we touched at least one versioned table
173: if (!dirtyTypes.isEmpty()) {
174: // first write down modified envelope
175: Feature f = null;
176: FeatureWriter writer = null;
177: try {
178: // build filter to extract the appropriate changeset record
179: FilterFactory ff = CommonFactoryFinder
180: .getFilterFactory(null);
181: Filter revisionFilter = ff.id(Collections.singleton(ff
182: .featureId(String.valueOf(getRevision()))));
183:
184: // get a writer for the changeset record we want to update
185: writer = wrapped.getFeatureWriter(
186: VersionedPostgisDataStore.TBL_CHANGESETS,
187: (org.geotools.filter.Filter) revisionFilter,
188: transaction);
189: if (!writer.hasNext()) {
190: // who ate my changeset record ?!?
191: throw new IOException(
192: "Could not find the changeset record "
193: + "that should have been set in the versioned datastore on "
194: + "versioned jdbc state creation");
195: }
196:
197: // update it
198: f = writer.next();
199: f.setDefaultGeometry(toLatLonRectange(bbox));
200: writer.write();
201: } catch (IllegalAttributeException e) {
202: // if this happens there's a programming error
203: throw new DataSourceException(
204: "Could not set an attribute in changesets, "
205: + "most probably the table schema has been tampered with.",
206: e);
207: } finally {
208: if (writer != null)
209: writer.close();
210: }
211:
212: // then write down the modified feature types
213: Statement st = null;
214: try {
215: st = getConnection().createStatement();
216: for (Iterator it = dirtyTypes.iterator(); it.hasNext();) {
217: String typeName = (String) it.next();
218: execute(
219: st,
220: "INSERT INTO "
221: + VersionedPostgisDataStore.TBL_TABLESCHANGED
222: + " "
223: + "SELECT "
224: + revision
225: + ", id "
226: + "FROM "
227: + VersionedPostgisDataStore.TBL_VERSIONEDTABLES
228: + " WHERE SCHEMA = '"
229: + wrapped.getConfig()
230: .getDatabaseSchemaName()
231: + "' " + "AND NAME = '" + typeName
232: + "'");
233: }
234: } catch (SQLException e) {
235: throw new DataSourceException(
236: "Error occurred while trying to save modified tables for "
237: + "this changeset. This should not happen, probaly there's a "
238: + "bug at work here.", e);
239: } finally {
240: JDBCUtils.close(st);
241: }
242: }
243:
244: // aah, all right, now we can really commit this transaction and be happy
245: super .commit();
246: // reset revision, we create a new revision for each new commit
247: reset();
248: }
249:
250: public boolean isRevisionSet() {
251: return revision == Long.MIN_VALUE;
252: }
253:
254: /**
255: * Takes a referenced envelope and turns it into a lat/lon Polygon
256: *
257: * @param envelope
258: * @return
259: * @throws TransformException
260: */
261: Geometry toLatLonRectange(final ReferencedEnvelope env)
262: throws IOException {
263: ReferencedEnvelope envelope = new ReferencedEnvelope(env);
264: try {
265: // since we cannot work with a null geometry in commits to
266: // changesets, let's return a very small envelope...
267: // an empty envelope gets turned into a point
268: if (envelope == null || envelope.isEmpty()) {
269: envelope = new ReferencedEnvelope(new Envelope(0, EPS,
270: 0, EPS), DefaultGeographicCRS.WGS84);
271: } else {
272: envelope = envelope.transform(
273: DefaultGeographicCRS.WGS84, true);
274: if (envelope.getHeight() == 0.0
275: || envelope.getWidth() == 0.0)
276: envelope.expandBy(EPS);
277: }
278:
279: GeometryFactory gf = new GeometryFactory();
280: return gf.toGeometry(envelope);
281: } catch (Exception e) {
282: throw new DataSourceException(
283: "An error occurred while trying to builds a "
284: + "lat/lon polygon equivalent to "
285: + envelope, e);
286: }
287: }
288:
289: /**
290: * Stores a commit message in the CHANGESETS table and return the associated revision number.
291: * TODO: this may well be moved to the {@link VersionedJdbcTransactionState} class?
292: *
293: * @param conn
294: * @return
295: * @throws IOException
296: */
297: protected long writeRevision(Transaction t, ReferencedEnvelope bbox)
298: throws IOException {
299: Feature f = null;
300: FeatureWriter writer = null;
301: String author = (String) t
302: .getProperty(VersionedPostgisDataStore.AUTHOR);
303: String message = (String) t
304: .getProperty(VersionedPostgisDataStore.MESSAGE);
305: try {
306: writer = wrapped.getFeatureWriterAppend(
307: VersionedPostgisDataStore.TBL_CHANGESETS, t);
308: f = writer.next();
309: f.setAttribute("author", author);
310: f.setAttribute("message", message);
311: f.setAttribute("date", new Date());
312:
313: f.setDefaultGeometry(toLatLonRectange(bbox));
314: writer.write();
315: } catch (IllegalAttributeException e) {
316: // if this happens there's a programming error
317: throw new IOException(
318: "Could not set an attribute in changesets, "
319: + "most probably the table schema has been tampered with.");
320: } finally {
321: if (writer != null)
322: writer.close();
323: }
324:
325: return ((Long) f.getAttribute("revision")).longValue();
326: }
327:
328: /**
329: * Logs the sql at info level, then executes the command
330: *
331: * @param st
332: * @param sql
333: * @throws SQLException
334: */
335: protected void execute(Statement st, String sql)
336: throws SQLException {
337: LOGGER.fine(sql);
338: st.execute(sql);
339: }
340:
341: /**
342: * Returns the transaction associated to this state
343: *
344: * @return
345: */
346: Transaction getTransaction() {
347: return transaction;
348: }
349: }
|