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.SQLException;
020: import java.sql.Statement;
021:
022: import org.geotools.data.DataSourceException;
023: import org.geotools.data.DataUtilities;
024: import org.geotools.data.FeatureListenerManager;
025: import org.geotools.data.FeatureLockException;
026: import org.geotools.data.FeatureWriter;
027: import org.geotools.data.jdbc.JDBCUtils;
028: import org.geotools.data.jdbc.MutableFIDFeature;
029: import org.geotools.data.postgis.fidmapper.VersionedFIDMapper;
030: import org.geotools.feature.AttributeType;
031: import org.geotools.feature.DefaultFeatureType;
032: import org.geotools.feature.Feature;
033: import org.geotools.feature.FeatureType;
034: import org.geotools.feature.GeometryAttributeType;
035: import org.geotools.feature.IllegalAttributeException;
036: import org.geotools.geometry.jts.JTS;
037: import org.opengis.referencing.crs.CoordinateReferenceSystem;
038: import org.opengis.referencing.operation.TransformException;
039:
040: import com.vividsolutions.jts.geom.Envelope;
041: import com.vividsolutions.jts.geom.Geometry;
042:
043: /**
044: * A feature writer that handles versioning using two slave feature writers to expire old features
045: * and create new revisions of the features
046: *
047: * @author aaime
048: * @since 2.4
049: *
050: */
051: class VersionedFeatureWriter implements FeatureWriter {
052:
053: private static Long NON_EXPIRED = new Long(Long.MAX_VALUE);
054:
055: private FeatureWriter updateWriter;
056:
057: private FeatureWriter appendWriter;
058:
059: private FeatureType featureType;
060:
061: private Feature oldFeature;
062:
063: private Feature newFeature;
064:
065: private Feature liveFeature;
066:
067: private VersionedJdbcTransactionState state;
068:
069: private VersionedFIDMapper mapper;
070:
071: private FeatureListenerManager listenerManager;
072:
073: private boolean autoCommit;
074:
075: /**
076: * Builds a new feature writer
077: *
078: * @param updateWriter
079: * @param appendWriter
080: * @param featureType
081: * the outside visible feature type
082: * @param mapper
083: * @param autoCommit
084: * if true, the transaction need to be committed once the writer is closed
085: */
086: public VersionedFeatureWriter(FeatureWriter updateWriter,
087: FeatureWriter appendWriter, FeatureType featureType,
088: VersionedJdbcTransactionState state,
089: VersionedFIDMapper mapper, boolean autoCommit) {
090: this .updateWriter = updateWriter;
091: this .appendWriter = appendWriter;
092: this .featureType = featureType;
093: this .state = state;
094: this .mapper = mapper;
095: this .autoCommit = autoCommit;
096: }
097:
098: public void setFeatureListenerManager(
099: FeatureListenerManager listenerManager) {
100: this .listenerManager = listenerManager;
101: }
102:
103: public void close() throws IOException {
104: if (updateWriter != null)
105: updateWriter.close();
106: appendWriter.close();
107:
108: // double check, state.getTransaction() will return null if the transaction
109: // has already been closed
110: if (autoCommit && state.getTransaction() != null) {
111: state.getTransaction().commit();
112: state.getTransaction().close();
113: }
114: }
115:
116: public FeatureType getFeatureType() {
117: return featureType;
118: }
119:
120: public boolean hasNext() throws IOException {
121: appendWriter.hasNext();
122: if (updateWriter != null)
123: return updateWriter.hasNext();
124: else
125: return false;
126: }
127:
128: public Feature next() throws IOException {
129: Feature original = null;
130: if (updateWriter != null && updateWriter.hasNext()) {
131: oldFeature = updateWriter.next();
132: newFeature = appendWriter.next();
133: original = oldFeature;
134: state
135: .expandDirtyBounds(getLatLonFeatureEnvelope(oldFeature));
136: } else {
137: oldFeature = null;
138: newFeature = appendWriter.next();
139: original = newFeature;
140: }
141:
142: try {
143: liveFeature = DataUtilities.reType(featureType, original);
144: // if the feature it brand new, it'll have a random fid, not a
145: // proper one, keep using
146: // it, we cannot un-version it
147: String unversionedId = liveFeature.getID();
148: if (oldFeature != null)
149: unversionedId = mapper.getUnversionedFid(liveFeature
150: .getID());
151: liveFeature = new MutableFIDFeature(
152: (DefaultFeatureType) featureType, liveFeature
153: .getAttributes(new Object[featureType
154: .getAttributeCount()]),
155: unversionedId);
156: return liveFeature;
157: } catch (IllegalAttributeException e) {
158: throw new DataSourceException(
159: "Error casting versioned feature to external one. "
160: + "Should not happen, there's a bug at work",
161: e);
162: }
163: }
164:
165: /**
166: * Computes a feature's envelope, using all geometry attributes, and returns an envelop in WGS84
167: *
168: * @param oldFeature
169: * @return
170: * @throws TransformException
171: */
172: public Envelope getLatLonFeatureEnvelope(Feature feature)
173: throws IOException {
174: try {
175: Envelope result = new Envelope();
176: FeatureType ft = feature.getFeatureType();
177: for (int i = 0; i < ft.getAttributeCount(); i++) {
178: AttributeType at = ft.getAttributeType(i);
179: if (at instanceof GeometryAttributeType) {
180: GeometryAttributeType gat = (GeometryAttributeType) at;
181: CoordinateReferenceSystem crs = gat
182: .getCoordinateSystem();
183:
184: Geometry geom = (Geometry) feature.getAttribute(i);
185: if (geom != null) {
186: Envelope env = geom.getEnvelopeInternal();
187: if (crs != null)
188: env = JTS.toGeographic(env, crs);
189: result.expandToInclude(env);
190: }
191: }
192: }
193: return result;
194: } catch (TransformException e) {
195: throw new DataSourceException(
196: "Error computing lat/long envelope of the current feature. "
197: + "This is needed to update the changeset bbox",
198: e);
199: }
200: }
201:
202: public void remove() throws IOException {
203: // if the feature is new, we have nothing to remove
204: if (oldFeature == null) {
205: throw new IOException("No feature available to remove");
206: }
207:
208: listenerManager.fireFeaturesRemoved(getFeatureType()
209: .getTypeName(), state.getTransaction(), oldFeature
210: .getBounds(), false);
211: writeOldFeature(true);
212: }
213:
214: private void writeOldFeature(boolean expire) throws IOException,
215: DataSourceException {
216: try {
217: if (expire)
218: oldFeature.setAttribute("expired", new Long(state
219: .getRevision()));
220: updateWriter.write();
221: } catch (IllegalAttributeException e) {
222: throw new DataSourceException(
223: "Error writing expiration tag on old feature. "
224: + "Should not happen, there's a bug at work.",
225: e);
226: } catch (FeatureLockException fle) {
227: // we have to mangle the id here too
228: String unversionedFid = mapper.getUnversionedFid(fle
229: .getFeatureID());
230: FeatureLockException mangled = new FeatureLockException(fle
231: .getMessage(), unversionedFid, fle.getCause());
232: throw mangled;
233: }
234: }
235:
236: public void write() throws IOException {
237: Statement st = null;
238: try {
239: /*
240: Ok, this is complex. We have to deal with four separate cases:
241: 1) the old feature is not there, meaning we're inserting a new feature
242: 2) the old feature is there, the new feature is equal to the old one -> no changes,
243: let's just move on
244: 3) the old feature is there, and it's the first time we modify that feature
245: in this transactions, meaning we need to expire the old feature, and
246: create a new, non expired one
247: 4) the old feature is there, but we already modified it during this transaction. This
248: means we have to update the old feature
249: */
250:
251: boolean dirtyFeature = false;
252: if (oldFeature != null) {
253: // if there is an old feature, make sure to write a new revision only if the
254: // feauture was modified
255: boolean dirty = false;
256: for (int i = 0; i < liveFeature.getNumberOfAttributes(); i++) {
257: AttributeType at = liveFeature.getFeatureType()
258: .getAttributeType(i);
259: Object newValue = liveFeature.getAttribute(at
260: .getName());
261: Object oldValue = oldFeature.getAttribute(at
262: .getName());
263: newFeature.setAttribute(at.getName(), newValue);
264: if (!DataUtilities.attributesEqual(newValue,
265: oldValue)) {
266: dirty = true;
267: }
268: }
269: if (!dirty)
270: return;
271:
272: // check if the feature is dirty. The live feature has the right external id
273: String typeName = liveFeature.getFeatureType()
274: .getTypeName();
275: String fid = liveFeature.getID();
276: dirtyFeature = state.isFidDirty(typeName, fid);
277: }
278:
279: Feature writtenFeature = null;
280: if (dirtyFeature) {
281: // we're updating again a feature we already touched, so we have to move
282: // attributes from the live to the old, and make sure the old is not expired
283: // (we may have deleted and then re-inserted that feature, if FID are the user
284: // assigned kind we can get into troubles with duplicated primary keys)
285:
286: // copy attributes from live to new
287: for (int i = 0; i < liveFeature.getNumberOfAttributes(); i++) {
288: AttributeType at = liveFeature.getFeatureType()
289: .getAttributeType(i);
290: oldFeature.setAttribute(at.getName(), liveFeature
291: .getAttribute(at.getName()));
292: }
293:
294: // write the old one
295: writeOldFeature(false);
296:
297: writtenFeature = oldFeature;
298: } else {
299: // expire if needed
300: if (oldFeature != null)
301: writeOldFeature(true);
302:
303: // copy attributes from live to new
304: for (int i = 0; i < liveFeature.getNumberOfAttributes(); i++) {
305: AttributeType at = liveFeature.getFeatureType()
306: .getAttributeType(i);
307: newFeature.setAttribute(at.getName(), liveFeature
308: .getAttribute(at.getName()));
309: }
310:
311: //set revision and expired,
312: newFeature.setAttribute("expired", NON_EXPIRED);
313: newFeature.setAttribute("revision", new Long(state
314: .getRevision()));
315:
316: // mark the feature creation
317: if (oldFeature != null) {
318: newFeature.setAttribute("created", oldFeature
319: .getAttribute("created"));
320: } else {
321: newFeature.setAttribute("created", new Long(state
322: .getRevision()));
323: }
324:
325: // set FID to the old one
326: // TODO: check this, I'm not sure this is the proper handling
327: String id = null;
328: if (oldFeature != null) {
329: id = mapper.createVersionedFid(liveFeature.getID(),
330: state.getRevision());
331: newFeature.setAttribute("created", oldFeature
332: .getAttribute("created"));
333: } else if (!mapper.hasAutoIncrementColumns()) {
334: id = mapper.createID(state.getConnection(),
335: newFeature, null);
336: newFeature.setAttribute("created", new Long(state
337: .getRevision()));
338: }
339: // transfer generated id values to the primary key attributes
340: if (id != null) {
341: ((MutableFIDFeature) newFeature).setID(id);
342:
343: Object[] pkatts = mapper.getPKAttributes(id);
344: for (int i = 0; i < pkatts.length; i++) {
345: newFeature.setAttribute(
346: mapper.getColumnName(i), pkatts[i]);
347: }
348: }
349:
350: // write
351: appendWriter.write();
352:
353: // if the id is auto-generated, gather it from the db
354: if (oldFeature == null
355: && mapper.hasAutoIncrementColumns()) {
356: st = state.getConnection().createStatement();
357: id = mapper.createID(state.getConnection(),
358: newFeature, st);
359: }
360:
361: // make sure the newly generated id is set into the live
362: // feature, and that it's typed, too
363: ((MutableFIDFeature) newFeature).setID(id);
364: ((MutableFIDFeature) liveFeature).setID(mapper
365: .getUnversionedFid(id));
366:
367: // mark the fid as dirty
368: state.setFidDirty(liveFeature.getFeatureType()
369: .getTypeName(), liveFeature.getID());
370:
371: writtenFeature = newFeature;
372: }
373:
374: // update dirty bounds
375: state
376: .expandDirtyBounds(getLatLonFeatureEnvelope(writtenFeature));
377:
378: // and finally notify the user
379: if (oldFeature != null) {
380: Envelope bounds = oldFeature.getBounds();
381: bounds.expandToInclude(liveFeature.getBounds());
382: listenerManager.fireFeaturesChanged(getFeatureType()
383: .getTypeName(), state.getTransaction(), bounds,
384: false);
385: } else {
386: listenerManager.fireFeaturesAdded(getFeatureType()
387: .getTypeName(), state.getTransaction(),
388: liveFeature.getBounds(), false);
389: }
390: } catch (IllegalAttributeException e) {
391: throw new DataSourceException(
392: "Error writing expiration tag on old feature. "
393: + "Should not happen, there's a bug at work.",
394: e);
395: } catch (SQLException e) {
396: throw new DataSourceException(
397: "Error creating a new statement for primary key generation",
398: e);
399: } finally {
400: JDBCUtils.close(st);
401: }
402: }
403:
404: }
|