001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_testing/src/org/deegree/io/datastore/sql/transaction/UpdateHandler.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree, http://deegree.org/
004     Copyright (C) 2001-2009 by:
005     Department of Geography, University of Bonn
006     and
007     lat/lon GmbH
008    
009     This library is free software; you can redistribute it and/or modify it under
010     the terms of the GNU Lesser General Public License as published by the Free
011     Software Foundation; either version 2.1 of the License, or (at your option)
012     any later version.
013     This library is distributed in the hope that it will be useful, but WITHOUT
014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016     details.
017     You should have received a copy of the GNU Lesser General Public License
018     along with this library; if not, write to the Free Software Foundation, Inc.,
019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020    
021     Contact information:
022    
023     lat/lon GmbH
024     Aennchenstr. 19, 53177 Bonn
025     Germany
026     http://lat-lon.de/
027    
028     Department of Geography, University of Bonn
029     Prof. Dr. Klaus Greve
030     Postfach 1147, 53001 Bonn
031     Germany
032     http://www.geographie.uni-bonn.de/deegree/
033    
034     e-mail: info@deegree.org
035     ----------------------------------------------------------------------------*/
036    package org.deegree.io.datastore.sql.transaction;
037    
038    import java.sql.Connection;
039    import java.sql.PreparedStatement;
040    import java.sql.ResultSet;
041    import java.sql.SQLException;
042    import java.util.ArrayList;
043    import java.util.Enumeration;
044    import java.util.Hashtable;
045    import java.util.LinkedHashMap;
046    import java.util.List;
047    import java.util.Map;
048    
049    import org.deegree.datatypes.QualifiedName;
050    import org.deegree.datatypes.Types;
051    import org.deegree.framework.log.ILogger;
052    import org.deegree.framework.log.LoggerFactory;
053    import org.deegree.i18n.Messages;
054    import org.deegree.io.datastore.DatastoreException;
055    import org.deegree.io.datastore.FeatureId;
056    import org.deegree.io.datastore.TransactionException;
057    import org.deegree.io.datastore.idgenerator.FeatureIdAssigner;
058    import org.deegree.io.datastore.schema.MappedFeaturePropertyType;
059    import org.deegree.io.datastore.schema.MappedFeatureType;
060    import org.deegree.io.datastore.schema.MappedGeometryPropertyType;
061    import org.deegree.io.datastore.schema.MappedPropertyType;
062    import org.deegree.io.datastore.schema.MappedSimplePropertyType;
063    import org.deegree.io.datastore.schema.TableRelation;
064    import org.deegree.io.datastore.schema.TableRelation.FK_INFO;
065    import org.deegree.io.datastore.schema.content.MappingField;
066    import org.deegree.io.datastore.schema.content.MappingGeometryField;
067    import org.deegree.io.datastore.schema.content.SimpleContent;
068    import org.deegree.io.datastore.sql.AbstractRequestHandler;
069    import org.deegree.io.datastore.sql.StatementBuffer;
070    import org.deegree.io.datastore.sql.TableAliasGenerator;
071    import org.deegree.io.datastore.sql.transaction.delete.DeleteHandler;
072    import org.deegree.io.datastore.sql.transaction.insert.InsertHandler;
073    import org.deegree.model.feature.Feature;
074    import org.deegree.model.feature.FeatureProperty;
075    import org.deegree.model.feature.schema.FeaturePropertyType;
076    import org.deegree.model.feature.schema.PropertyType;
077    import org.deegree.model.filterencoding.Filter;
078    import org.deegree.model.spatialschema.Geometry;
079    import org.deegree.ogcbase.ElementStep;
080    import org.deegree.ogcbase.PropertyPath;
081    import org.deegree.ogcbase.PropertyPathStep;
082    import org.deegree.ogcwebservices.wfs.operation.transaction.Insert;
083    import org.deegree.ogcwebservices.wfs.operation.transaction.Transaction;
084    import org.deegree.ogcwebservices.wfs.operation.transaction.Update;
085    
086    /**
087     * Handler for {@link Update} operations (usually contained in {@link Transaction} requests).
088     * 
089     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
090     * @author last edited by: $Author: rbezema $
091     * 
092     * @version $Revision: 19436 $, $Date: 2009-08-31 16:37:44 +0200 (Mo, 31. Aug 2009) $
093     */
094    public class UpdateHandler extends AbstractRequestHandler {
095    
096        private static final ILogger LOG = LoggerFactory.getLogger( UpdateHandler.class );
097    
098        private SQLTransaction dsTa;
099    
100        private String lockId;
101    
102        /**
103         * Creates a new <code>UpdateHandler</code> from the given parameters.
104         * 
105         * @param dsTa
106         * @param aliasGenerator
107         * @param conn
108         * @param lockId
109         *            optional id of associated lock (may be null)
110         */
111        public UpdateHandler( SQLTransaction dsTa, TableAliasGenerator aliasGenerator, Connection conn, String lockId ) {
112            super( dsTa.getDatastore(), aliasGenerator, conn );
113            this.dsTa = dsTa;
114            this.lockId = lockId;
115        }
116    
117        /**
118         * Performs an update operation against the associated datastore.
119         * 
120         * @param ft
121         * @param replacementProps
122         * @param filter
123         * @return number of updated (root) feature instances
124         * @throws DatastoreException
125         */
126        public int performUpdate( MappedFeatureType ft, Map<PropertyPath, FeatureProperty> replacementProps, Filter filter )
127                                throws DatastoreException {
128    
129            List<FeatureId> fids = determineAffectedAndModifiableFIDs( ft, filter, this.lockId );
130    
131            LOG.logDebug( "Updating: " + ft );
132            for ( FeatureId fid : fids ) {
133                Hashtable<String, Hashtable<FeatureId, StatementBuffer>> tableToFeatureUpdate = new Hashtable<String, Hashtable<FeatureId, StatementBuffer>>();
134    
135                for ( PropertyPath property : replacementProps.keySet() ) {
136                    LOG.logDebug( "Updating feature: " + fid );
137                    FeatureProperty propertyValue = replacementProps.get( property );
138                    performUpdate( fid, ft, property, propertyValue, tableToFeatureUpdate );
139                }
140    
141                for ( Hashtable<FeatureId, StatementBuffer> featureStatementBuffers : tableToFeatureUpdate.values() ) {
142                    for ( Enumeration<FeatureId> it = featureStatementBuffers.keys(); it.hasMoreElements(); ) {
143                        FeatureId statementFid = it.nextElement();
144                        StatementBuffer query = featureStatementBuffers.get( statementFid );
145    
146                        query.append( " WHERE " );
147                        appendFIDWhereCondition( query, statementFid );
148    
149                        PreparedStatement stmt = null;
150                        LOG.logDebug( "Performing aggregate update of in-table properties: " + query.getQueryString() );
151                        try {
152                            stmt = this.datastore.prepareStatement( conn, query );
153                            stmt.execute();
154                        } catch ( SQLException e ) {
155                            throw new DatastoreException( "Error in performUpdate(): " + e.getMessage() );
156                        } finally {
157                            if ( stmt != null ) {
158                                try {
159                                    stmt.close();
160                                } catch ( SQLException e ) {
161                                    LOG.logError( "Error closing statement: '" + e.getMessage() + "'.", e );
162                                }
163                            }
164                        }
165                    }
166                }
167            }
168            return fids.size();
169        }
170    
171        /**
172         * Performs an update operation (replace-style) against the associated datastore.
173         * <p>
174         * All features matched by the given filter are altered, so their properties are identical to those of the specified
175         * replacement feature.
176         * </p>
177         * <p>
178         * NOTE: Currently, the contained feature must not contain any feature-valued properties or multi-properties.
179         * </p>
180         * 
181         * @param mappedFeatureType
182         * @param replacementFeature
183         * @param filter
184         * @return number of updated (root) feature instances
185         * @throws DatastoreException
186         */
187        public int performUpdate( MappedFeatureType mappedFeatureType, Feature replacementFeature, Filter filter )
188                                throws DatastoreException {
189            LOG.logDebug( "Updating (replace): " + mappedFeatureType );
190            if ( filter != null ) {
191                LOG.logDebug( " filter: " + filter.to110XML() );
192            }
193    
194            PropertyType[] replPropertyTypes = replacementFeature.getFeatureType().getProperties();
195            int result = 0;
196            // rb: updating complex features is a needed feature, gml:id reservation as well, therefore let's see if we are
197            // updating a complex feature, we do the old school, delete->insert (loosing gml:id) if updating a simple
198            // feature, do a 'new school' update.
199            if ( replPropertyTypes != null ) {
200                boolean oldSchool = false;
201                for ( int i = 0; i < replPropertyTypes.length && !oldSchool; ++i ) {
202                    PropertyType rpt = replPropertyTypes[i];
203                    oldSchool = rpt != null
204                                && ( rpt instanceof FeaturePropertyType || rpt instanceof MappedFeaturePropertyType );
205    
206                }
207                if ( oldSchool ) {
208                    LOG.logWarning( "The given featuretype, is a complex feature type, updating this feature will result in the loss of gml:id's in the update feature and it's references." );
209                    result = performUpdateWithFeatures( mappedFeatureType, replacementFeature, filter );
210                } else {
211                    LOG.logDebug( "Updating feature with correct gml:id handling (replace): " + mappedFeatureType );
212                    result = performUpdateCorrectForProperties( mappedFeatureType, replacementFeature, filter );
213                }
214            }
215            return result;
216        }
217    
218        /**
219         * Performs an update operation (replace-style) against the associated datastore.
220         * <p>
221         * All features matched by the given filter are altered, so their properties are identical to those of the specified
222         * replacement feature.
223         * </p>
224         * <p>
225         * NOTE: Currently, the contained feature must not contain any feature-valued properties or multi-properties.
226         * </p>
227         * 
228         * @param mappedFeatureType
229         * @param replacementFeature
230         * @param filter
231         * @return number of updated (root) feature instances
232         * @throws DatastoreException
233         */
234        private int performUpdateCorrectForProperties( MappedFeatureType mappedFeatureType, Feature replacementFeature,
235                                                       Filter filter )
236                                throws DatastoreException {
237    
238            Map<PropertyPath, FeatureProperty> replaceProperties = new LinkedHashMap<PropertyPath, FeatureProperty>();
239            FeatureProperty[] featureProps = replacementFeature.getProperties();
240            for ( FeatureProperty featureProperty : featureProps ) {
241                List<PropertyPathStep> steps = new ArrayList<PropertyPathStep>( 1 );
242                steps.add( new ElementStep( featureProperty.getName() ) );
243                PropertyPath path = new PropertyPath( steps );
244                replaceProperties.put( path, featureProperty );
245            }
246            return performUpdate( mappedFeatureType, replaceProperties, filter );
247        }
248    
249        /**
250         * Performs an update operation against the associated datastore.
251         * <p>
252         * The filter must match exactly one feature instance (or none) which is then replaced by the specified replacement
253         * feature.
254         * 
255         * @param mappedFeatureType
256         * @param replacementFeature
257         * @param filter
258         * @return number of updated (root) feature instances (0 or 1)
259         * @throws DatastoreException
260         */
261        private int performUpdateWithFeatures( MappedFeatureType mappedFeatureType, Feature replacementFeature,
262                                               Filter filter )
263                                throws DatastoreException {
264    
265            List<FeatureId> fids = determineAffectedAndModifiableFIDs( mappedFeatureType, filter, this.lockId );
266    
267            if ( fids.size() > 1 ) {
268                String msg = Messages.getMessage( "DATASTORE_MORE_THAN_ONE_FEATURE" );
269                throw new DatastoreException( msg );
270            }
271            DeleteHandler deleteHandler = new DeleteHandler( this.dsTa, this.aliasGenerator, this.conn, this.lockId );
272            deleteHandler.performDelete( mappedFeatureType, filter );
273    
274            // identify stored subfeatures / assign feature ids
275            FeatureIdAssigner fidAssigner = new FeatureIdAssigner( Insert.ID_GEN.GENERATE_NEW );
276            fidAssigner.assignFID( replacementFeature, this.dsTa );
277            // TODO remove this hack
278            fidAssigner.markStoredFeatures();
279    
280            InsertHandler insertHandler = new InsertHandler( this.dsTa, this.aliasGenerator, this.conn );
281            List<Feature> features = new ArrayList<Feature>();
282            features.add( replacementFeature );
283            insertHandler.performInsert( features );
284    
285            return fids.size();
286        }
287    
288        /**
289         * Performs the update (replacing of a property) of the given feature instance.
290         * <p>
291         * If the selected property is a direct property of the feature, the root feature is updated, otherwise the targeted
292         * subfeatures have to be determined first.
293         * 
294         * @param fid
295         * @param ft
296         * @param propertyName
297         * @param replacementProperty
298         * @param statementBuffers
299         * @throws DatastoreException
300         */
301        private void performUpdate( FeatureId fid, MappedFeatureType ft, PropertyPath propertyName,
302                                    FeatureProperty replacementProperty,
303                                    Hashtable<String, Hashtable<FeatureId, StatementBuffer>> statementBuffers )
304                                throws DatastoreException {
305    
306            Object replacementValue = replacementProperty.getValue();
307            LOG.logDebug( "Updating fid: " + fid + ", propertyName: " + propertyName + " -> " + replacementValue );
308    
309            int steps = propertyName.getSteps();
310            QualifiedName propName = propertyName.getStep( steps - 1 ).getPropertyName();
311            if ( steps > 2 ) {
312                QualifiedName subFtName = propertyName.getStep( steps - 2 ).getPropertyName();
313                MappedFeatureType subFt = this.datastore.getFeatureType( subFtName );
314                MappedPropertyType pt = (MappedPropertyType) subFt.getProperty( propName );
315                List<TableRelation> tablePath = getTablePath( ft, propertyName );
316                List<FeatureId> subFids = determineAffectedFIDs( fid, subFt, tablePath );
317                for ( FeatureId subFid : subFids ) {
318                    updateProperty( subFid, subFt, pt, replacementValue, statementBuffers );
319                }
320            } else {
321                MappedPropertyType pt = (MappedPropertyType) ft.getProperty( propName );
322                updateProperty( fid, ft, pt, replacementValue, statementBuffers );
323            }
324        }
325    
326        /**
327         * Determines the subfeature instances that are targeted by the given PropertyName.
328         * 
329         * @param fid
330         * @param subFt
331         * @param path
332         * @return the matched feature ids
333         * @throws DatastoreException
334         */
335        private List<FeatureId> determineAffectedFIDs( FeatureId fid, MappedFeatureType subFt, List<TableRelation> path )
336                                throws DatastoreException {
337    
338            List<FeatureId> subFids = new ArrayList<FeatureId>();
339    
340            this.aliasGenerator.reset();
341            String[] tableAliases = this.aliasGenerator.generateUniqueAliases( path.size() + 1 );
342            String toTableAlias = tableAliases[tableAliases.length - 1];
343            StatementBuffer query = new StatementBuffer();
344            query.append( "SELECT " );
345            appendFeatureIdColumns( subFt, toTableAlias, query );
346            query.append( " FROM " );
347            query.append( path.get( 0 ).getFromTable() );
348            query.append( " " );
349            query.append( tableAliases[0] );
350            // append joins
351            for ( int i = 0; i < path.size(); i++ ) {
352                query.append( " JOIN " );
353                query.append( path.get( i ).getToTable() );
354                query.append( " " );
355                query.append( tableAliases[i + 1] );
356                query.append( " ON " );
357                MappingField[] fromFields = path.get( i ).getFromFields();
358                MappingField[] toFields = path.get( i ).getToFields();
359                for ( int j = 0; j < fromFields.length; j++ ) {
360                    query.append( tableAliases[i] );
361                    query.append( '.' );
362                    query.append( fromFields[j].getField() );
363                    query.append( '=' );
364                    query.append( tableAliases[i + 1] );
365                    query.append( '.' );
366                    query.append( toFields[j].getField() );
367                }
368            }
369            query.append( " WHERE " );
370            MappingField[] fidFields = fid.getFidDefinition().getIdFields();
371            for ( int i = 0; i < fidFields.length; i++ ) {
372                query.append( tableAliases[0] );
373                query.append( '.' );
374                query.append( fidFields[i].getField() );
375                query.append( "=?" );
376                query.addArgument( fid.getValue( i ), fidFields[i].getType() );
377                if ( i != fidFields.length - 1 ) {
378                    query.append( " AND " );
379                }
380            }
381    
382            PreparedStatement stmt = null;
383            ResultSet rs = null;
384            try {
385                stmt = this.datastore.prepareStatement( conn, query );
386                rs = stmt.executeQuery();
387                subFids = extractFeatureIds( rs, subFt );
388            } catch ( SQLException e ) {
389                throw new DatastoreException( "Error in determineAffectedFIDs(): " + e.getMessage() );
390            } finally {
391                try {
392                    if ( rs != null ) {
393                        try {
394                            rs.close();
395                        } catch ( SQLException e ) {
396                            LOG.logError( "Error closing result set: '" + e.getMessage() + "'.", e );
397                        }
398                    }
399                } finally {
400                    if ( stmt != null ) {
401                        try {
402                            stmt.close();
403                        } catch ( SQLException e ) {
404                            LOG.logError( "Error closing statement: '" + e.getMessage() + "'.", e );
405                        }
406                    }
407                }
408            }
409            return subFids;
410        }
411    
412        /**
413         * Returns the relations (the "path") that lead from the feature type's table to the subfeature table which is
414         * targeted by the specified property name.
415         * 
416         * @param ft
417         *            source feature type
418         * @param path
419         *            property name
420         * @return relations that lead from the feature type's table to the subfeature table
421         */
422        private List<TableRelation> getTablePath( MappedFeatureType ft, PropertyPath path ) {
423            List<TableRelation> relations = new ArrayList<TableRelation>();
424            for ( int i = 1; i < path.getSteps() - 2; i += 2 ) {
425                QualifiedName propName = path.getStep( i ).getPropertyName();
426                MappedFeaturePropertyType pt = (MappedFeaturePropertyType) ft.getProperty( propName );
427                TableRelation[] tableRelations = pt.getTableRelations();
428                for ( int j = 0; j < tableRelations.length; j++ ) {
429                    relations.add( tableRelations[j] );
430                }
431                ft = pt.getFeatureTypeReference().getFeatureType();
432            }
433            return relations;
434        }
435    
436        /**
437         * Replaces the specified feature's property with the given value.
438         * 
439         * @param fid
440         * @param ft
441         * @param pt
442         * @param replacementValue
443         * @throws DatastoreException
444         */
445        private void updateProperty( FeatureId fid, MappedFeatureType ft, MappedPropertyType pt, Object replacementValue,
446                                     Hashtable<String, Hashtable<FeatureId, StatementBuffer>> statementBuffers )
447                                throws DatastoreException {
448    
449            LOG.logDebug( "Updating property '" + pt.getName() + "' of feature '" + fid + "'." );
450            if ( !ft.isUpdatable() ) {
451                String msg = Messages.getMessage( "DATASTORE_FT_NOT_UPDATABLE", ft.getName() );
452                throw new DatastoreException( msg );
453            }
454            TableRelation[] tablePath = pt.getTableRelations();
455            if ( pt instanceof MappedSimplePropertyType ) {
456                SimpleContent content = ( (MappedSimplePropertyType) pt ).getContent();
457                if ( content.isUpdateable() ) {
458                    if ( content instanceof MappingField ) {
459                        updateSimpleProperty( fid, tablePath, (MappingField) content, replacementValue, statementBuffers );
460                    }
461                } else {
462                    LOG.logInfo( "Ignoring property '" + pt.getName() + "' in update - content is virtual." );
463                }
464            } else if ( pt instanceof MappedGeometryPropertyType ) {
465                MappedGeometryPropertyType geomPt = (MappedGeometryPropertyType) pt;
466                MappingGeometryField dbField = geomPt.getMappingField();
467                Geometry deegreeGeometry = (Geometry) replacementValue;
468                Object dbGeometry;
469    
470                int createSrsCode = dbField.getSRS();
471                int targetSrsCode = -1;
472    
473                if ( deegreeGeometry.getCoordinateSystem() == null ) {
474                    LOG.logDebug( "No SRS information for geometry available. Assuming '" + geomPt.getSRS() + "'." );
475                } else if ( !geomPt.getSRS().toString().equals( deegreeGeometry.getCoordinateSystem().getIdentifier() ) ) {
476                    String msg = "Insert-Transformation: geometry srs: "
477                                 + deegreeGeometry.getCoordinateSystem().getIdentifier() + " -> property srs: "
478                                 + geomPt.getSRS();
479                    LOG.logDebug( msg );
480                    if ( createSrsCode == -1 ) {
481                        msg = Messages.getMessage( "DATASTORE_SRS_NOT_SPECIFIED", pt.getName(),
482                                                   deegreeGeometry.getCoordinateSystem(), geomPt.getSRS() );
483                        throw new TransactionException( msg );
484                    }
485                    try {
486                        createSrsCode = datastore.getNativeSRSCode( deegreeGeometry.getCoordinateSystem().getIdentifier() );
487                    } catch ( DatastoreException e ) {
488                        throw new TransactionException( e.getMessage(), e );
489                    }
490                    targetSrsCode = dbField.getSRS();
491                }
492    
493                try {
494                    dbGeometry = this.datastore.convertDeegreeToDBGeometry( deegreeGeometry, createSrsCode, this.conn );
495                } catch ( DatastoreException e ) {
496                    throw new TransactionException( e.getMessage(), e );
497                }
498    
499                // TODO remove this Oracle hack
500                if ( this.datastore.getClass().getName().contains( "OracleDatastore" ) ) {
501                    dbField = new MappingGeometryField( dbField.getTable(), dbField.getField(), Types.STRUCT,
502                                                        dbField.getSRS() );
503                }
504    
505                updateGeometryProperty( fid, tablePath, dbField, dbGeometry, targetSrsCode, statementBuffers );
506            } else if ( pt instanceof FeaturePropertyType ) {
507                updateProperty( fid, ft, (MappedFeaturePropertyType) pt, (Feature) replacementValue );
508            } else {
509                throw new DatastoreException( "Internal error: Properties with type '" + pt.getClass()
510                                              + "' are not handled in UpdateHandler." );
511            }
512        }
513    
514        /**
515         * Updates a simple property of the specified feature.
516         * <p>
517         * Three cases are distinguished (which all have to be handled differently):
518         * <ol>
519         * <li>property value stored in feature table</li>
520         * <li>property value stored in property table, fk in property table</li>
521         * <li>property value stored in property table, fk in feature table</li>
522         * </ol>
523         * 
524         * @param fid
525         * @param tablePath
526         * @param dbField
527         * @param replacementValue
528         * @throws DatastoreException
529         */
530        private void updateSimpleProperty( FeatureId fid, TableRelation[] tablePath, MappingField dbField,
531                                           Object replacementValue,
532                                           Hashtable<String, Hashtable<FeatureId, StatementBuffer>> statementBuffers )
533                                throws DatastoreException {
534    
535            if ( tablePath.length == 0 ) {
536                updateSimpleProperty( fid, dbField, replacementValue, statementBuffers );
537            } else if ( tablePath.length == 1 ) {
538                TableRelation relation = tablePath[0];
539                if ( tablePath[0].getFKInfo() == FK_INFO.fkIsToField ) {
540                    Object[] keyValues = determineKeyValues( fid, relation );
541                    if ( keyValues != null ) {
542                        deletePropertyRows( relation, keyValues );
543                    }
544                    if ( replacementValue != null ) {
545                        insertPropertyRow( relation, keyValues, dbField, replacementValue );
546                    }
547                } else {
548                    Object[] oldKeyValues = determineKeyValues( fid, relation );
549                    Object[] newKeyValues = findOrInsertPropertyRow( relation, dbField, replacementValue );
550                    updateFeatureRow( fid, relation, newKeyValues );
551                    if ( oldKeyValues != null ) {
552                        deleteOrphanedPropertyRows( relation, oldKeyValues );
553                    }
554                }
555            } else {
556                throw new DatastoreException( "Updating of properties that are stored in "
557                                              + "related tables using join tables is not " + "supported." );
558            }
559        }
560    
561        /**
562         * Updates a geometry property of the specified feature.
563         * <p>
564         * Three cases are distinguished (which all have to be handled differently):
565         * <ol>
566         * <li>property value stored in feature table</li>
567         * <li>property value stored in property table, fk in property table</li>
568         * <li>property value stored in property table, fk in feature table</li>
569         * </ol>
570         * 
571         * @param fid
572         * @param tablePath
573         * @param dbField
574         * @param replacementValue
575         * @param targetSrsCode
576         * @throws DatastoreException
577         */
578        private void updateGeometryProperty( FeatureId fid, TableRelation[] tablePath, MappingGeometryField dbField,
579                                             Object replacementValue, int targetSrsCode,
580                                             Hashtable<String, Hashtable<FeatureId, StatementBuffer>> statementBuffers )
581                                throws DatastoreException {
582    
583            if ( tablePath.length == 0 ) {
584                updateGeometryProperty( fid, dbField, replacementValue, targetSrsCode, statementBuffers );
585            } else if ( tablePath.length == 1 ) {
586                throw new DatastoreException( "Updating of geometry properties that are stored in "
587                                              + "related tables is not supported." );
588            } else {
589                throw new DatastoreException( "Updating of properties that are stored in "
590                                              + "related tables using join tables is not " + "supported." );
591            }
592        }
593    
594        private void updateFeatureRow( FeatureId fid, TableRelation relation, Object[] newKeyValues )
595                                throws DatastoreException {
596    
597            StatementBuffer query = new StatementBuffer();
598            query.append( "UPDATE " );
599            query.append( relation.getFromTable() );
600            query.append( " SET " );
601            MappingField[] fromFields = relation.getFromFields();
602            for ( int i = 0; i < newKeyValues.length; i++ ) {
603                query.append( fromFields[i].getField() );
604                query.append( "=?" );
605                query.addArgument( newKeyValues[i], fromFields[i].getType() );
606            }
607            query.append( " WHERE " );
608            appendFIDWhereCondition( query, fid );
609    
610            LOG.logDebug( "Performing update: " + query.getQueryString() );
611    
612            PreparedStatement stmt = null;
613            try {
614                stmt = this.datastore.prepareStatement( conn, query );
615                stmt.execute();
616            } catch ( SQLException e ) {
617                throw new DatastoreException( "Error in performUpdate(): " + e.getMessage() );
618            } finally {
619                if ( stmt != null ) {
620                    try {
621                        stmt.close();
622                    } catch ( SQLException e ) {
623                        LOG.logError( "Error closing statement: '" + e.getMessage() + "'.", e );
624                    }
625                }
626            }
627        }
628    
629        /**
630         * Updates a simple property of the specified feature.
631         * <p>
632         * This method handles the case where the property is stored in the feature table itself, so a single UPDATE
633         * statement is sufficient.
634         * 
635         * If there is already another UPDATE statement created for this feature and table than a column=value fragment is
636         * added to this statement.
637         * 
638         * @param fid
639         * @param dbField
640         * @param replacementValue
641         * @throws DatastoreException
642         */
643        private void updateSimpleProperty( FeatureId fid, MappingField dbField, Object replacementValue,
644                                           Hashtable<String, Hashtable<FeatureId, StatementBuffer>> statementBuffers ) {
645    
646            Hashtable<FeatureId, StatementBuffer> featureStatementBuffers = statementBuffers.get( dbField.getTable() );
647            StatementBuffer query = null;
648            if ( featureStatementBuffers == null ) {
649                featureStatementBuffers = new Hashtable<FeatureId, StatementBuffer>();
650                statementBuffers.put( dbField.getTable(), featureStatementBuffers );
651            } else {
652                query = featureStatementBuffers.get( fid );
653            }
654    
655            if ( query == null ) {
656                query = new StatementBuffer();
657                query.append( "UPDATE " );
658                query.append( dbField.getTable() );
659                query.append( " SET " );
660    
661                featureStatementBuffers.put( fid, query );
662            } else {
663                query.append( ", " );
664            }
665    
666            query.append( dbField.getField() );
667            query.append( "=?" );
668            query.addArgument( replacementValue, dbField.getType() );
669        }
670    
671        /**
672         * Updates a geometry property of the specified feature.
673         * <p>
674         * This method handles the case where the property is stored in the feature table itself, so a single UPDATE
675         * statement is sufficient.
676         * 
677         * If there is already another UPDATE statement created for this feature and table than a column=value fragment is
678         * added to this statement.
679         * 
680         * @param fid
681         * @param dbField
682         * @param replacementValue
683         * @param targetSrsCode
684         * @throws DatastoreException
685         */
686        private void updateGeometryProperty( FeatureId fid, MappingGeometryField dbField, Object replacementValue,
687                                             int targetSrsCode,
688                                             Hashtable<String, Hashtable<FeatureId, StatementBuffer>> statementBuffers )
689                                throws DatastoreException {
690    
691            Hashtable<FeatureId, StatementBuffer> featureStatementBuffers = statementBuffers.get( dbField.getTable() );
692            StatementBuffer query = null;
693            if ( featureStatementBuffers == null ) {
694                featureStatementBuffers = new Hashtable<FeatureId, StatementBuffer>();
695                statementBuffers.put( dbField.getTable(), featureStatementBuffers );
696            } else {
697                query = featureStatementBuffers.get( fid );
698            }
699    
700            if ( query == null ) {
701                query = new StatementBuffer();
702                query.append( "UPDATE " );
703                query.append( dbField.getTable() );
704                query.append( " SET " );
705    
706                featureStatementBuffers.put( fid, query );
707            } else {
708                query.append( ", " );
709            }
710    
711            query.append( dbField.getField() );
712            query.append( "=" );
713            String placeHolder = "?";
714            if ( targetSrsCode != -1 ) {
715                placeHolder = this.datastore.buildSRSTransformCall( "?", targetSrsCode );
716            }
717            query.append( placeHolder );
718            query.addArgument( replacementValue, dbField.getType() );
719        }
720    
721        /**
722         * Determines the values for the key columns that are referenced by the given table relation (as from fields).
723         * 
724         * @param fid
725         * @param relation
726         * @return the values for the key columns
727         * @throws DatastoreException
728         */
729        private Object[] determineKeyValues( FeatureId fid, TableRelation relation )
730                                throws DatastoreException {
731    
732            StatementBuffer query = new StatementBuffer();
733            query.append( "SELECT " );
734            MappingField[] fromFields = relation.getFromFields();
735            for ( int i = 0; i < fromFields.length; i++ ) {
736                query.append( fromFields[i].getField() );
737                if ( i != fromFields.length - 1 ) {
738                    query.append( ',' );
739                }
740            }
741            query.append( " FROM " );
742            query.append( relation.getFromTable() );
743            query.append( " WHERE " );
744            appendFIDWhereCondition( query, fid );
745    
746            Object[] keyValues = new Object[fromFields.length];
747            LOG.logDebug( "determineKeyValues: " + query.getQueryString() );
748            PreparedStatement stmt = null;
749            try {
750                stmt = this.datastore.prepareStatement( conn, query );
751                ResultSet rs = stmt.executeQuery();
752                if ( rs.next() ) {
753                    for ( int i = 0; i < keyValues.length; i++ ) {
754                        Object value = rs.getObject( i + 1 );
755                        if ( value != null ) {
756                            keyValues[i] = value;
757                        } else {
758                            keyValues = null;
759                            break;
760                        }
761                    }
762                } else {
763                    LOG.logError( "Internal error. Result is empty (no rows)." );
764                    throw new SQLException();
765                }
766            } catch ( SQLException e ) {
767                throw new DatastoreException( "Error in performUpdate(): " + e.getMessage() );
768            } finally {
769                if ( stmt != null ) {
770                    try {
771                        stmt.close();
772                    } catch ( SQLException e ) {
773                        LOG.logError( "Error closing statement: '" + e.getMessage() + "'.", e );
774                    }
775                }
776            }
777            return keyValues;
778        }
779    
780        private void deletePropertyRows( TableRelation relation, Object[] keyValues )
781                                throws DatastoreException {
782    
783            StatementBuffer query = new StatementBuffer();
784            query.append( "DELETE FROM " );
785            query.append( relation.getToTable() );
786            query.append( " WHERE " );
787            MappingField[] toFields = relation.getToFields();
788            for ( int i = 0; i < toFields.length; i++ ) {
789                query.append( toFields[i].getField() );
790                query.append( "=?" );
791                query.addArgument( keyValues[i], toFields[i].getType() );
792                if ( i != toFields.length - 1 ) {
793                    query.append( " AND " );
794                }
795            }
796    
797            PreparedStatement stmt = null;
798            LOG.logDebug( "deletePropertyRows: " + query.getQueryString() );
799            try {
800                stmt = this.datastore.prepareStatement( conn, query );
801                stmt.execute();
802            } catch ( SQLException e ) {
803                throw new DatastoreException( "Error in performUpdate(): " + e.getMessage() );
804            } finally {
805                if ( stmt != null ) {
806                    try {
807                        stmt.close();
808                    } catch ( SQLException e ) {
809                        LOG.logError( "Error closing statement: '" + e.getMessage() + "'.", e );
810                    }
811                }
812            }
813        }
814    
815        private void insertPropertyRow( TableRelation relation, Object[] keyValues, MappingField dbField,
816                                        Object replacementValue )
817                                throws DatastoreException {
818    
819            if ( keyValues == null ) {
820                if ( relation.getFromFields().length > 1 ) {
821                    throw new DatastoreException( "Key generation for compound keys is not supported." );
822                }
823                // generate new primary key
824                keyValues = new Object[1];
825                keyValues[0] = relation.getIdGenerator().getNewId( dsTa );
826            }
827    
828            StatementBuffer query = new StatementBuffer();
829            query.append( "INSERT INTO " );
830            query.append( relation.getToTable() );
831            query.append( " (" );
832            MappingField[] toFields = relation.getToFields();
833            for ( int i = 0; i < toFields.length; i++ ) {
834                query.append( toFields[i].getField() );
835                query.append( ',' );
836            }
837            query.append( dbField.getField() );
838            query.append( ") VALUES (" );
839            for ( int i = 0; i < toFields.length; i++ ) {
840                query.append( '?' );
841                query.addArgument( keyValues[i], toFields[i].getType() );
842                query.append( ',' );
843            }
844            query.append( "?)" );
845            query.addArgument( replacementValue, dbField.getType() );
846    
847            PreparedStatement stmt = null;
848            LOG.logDebug( "insertPropertyRow: " + query.getQueryString() );
849            try {
850                stmt = this.datastore.prepareStatement( conn, query );
851                stmt.execute();
852            } catch ( SQLException e ) {
853                throw new DatastoreException( "Error in performUpdate(): " + e.getMessage() );
854            } finally {
855                if ( stmt != null ) {
856                    try {
857                        stmt.close();
858                    } catch ( SQLException e ) {
859                        LOG.logError( "Error closing statement: '" + e.getMessage() + "'.", e );
860                    }
861                }
862            }
863        }
864    
865        /**
866         * Returns the foreign key value(s) for the row that stores the given property.
867         * <p>
868         * If the row already exists, the existing key is returned, otherwise a new row for the property is inserted first.
869         * 
870         * @param relation
871         * @param dbField
872         * @param replacementValue
873         * @return foreign key value(s) for the row that stores the given property
874         * @throws DatastoreException
875         */
876        private Object[] findOrInsertPropertyRow( TableRelation relation, MappingField dbField, Object replacementValue )
877                                throws DatastoreException {
878    
879            Object[] keyValues = null;
880    
881            if ( dbField.getType() != Types.GEOMETRY ) {
882                StatementBuffer query = new StatementBuffer();
883                query.append( "SELECT " );
884                MappingField[] toFields = relation.getToFields();
885                for ( int i = 0; i < toFields.length; i++ ) {
886                    query.append( toFields[i].getField() );
887                    if ( i != toFields.length - 1 ) {
888                        query.append( ',' );
889                    }
890                }
891                query.append( " FROM " );
892                query.append( relation.getToTable() );
893                query.append( " WHERE " );
894                query.append( dbField.getField() );
895                query.append( "=?" );
896                query.addArgument( replacementValue, dbField.getType() );
897    
898                PreparedStatement stmt = null;
899                LOG.logDebug( "findOrInsertPropertyRow: " + query.getQueryString() );
900                try {
901                    stmt = this.datastore.prepareStatement( conn, query );
902                    ResultSet rs = stmt.executeQuery();
903                    if ( rs.next() ) {
904                        keyValues = new Object[toFields.length];
905                        for ( int i = 0; i < toFields.length; i++ ) {
906                            keyValues[i] = rs.getObject( i + 1 );
907                        }
908                    }
909                } catch ( SQLException e ) {
910                    throw new DatastoreException( "Error in findOrInsertPropertyRow(): " + e.getMessage() );
911                } finally {
912                    if ( stmt != null ) {
913                        try {
914                            stmt.close();
915                        } catch ( SQLException e ) {
916                            LOG.logError( "Error closing statement: '" + e.getMessage() + "'.", e );
917                        }
918                    }
919                }
920                if ( keyValues != null ) {
921                    return keyValues;
922                }
923            }
924    
925            if ( relation.getToFields().length > 1 ) {
926                throw new DatastoreException( "Key generation for compound keys is not supported." );
927            }
928    
929            // property does not yet exist (or it's a geometry)
930            keyValues = new Object[1];
931            // generate new PK
932            keyValues[0] = relation.getNewPK( this.dsTa );
933            insertPropertyRow( relation, keyValues, dbField, replacementValue );
934    
935            return keyValues;
936        }
937    
938        private void deleteOrphanedPropertyRows( TableRelation relation, Object[] keyValues )
939                                throws DatastoreException {
940            DeleteHandler deleteHandler = new DeleteHandler( this.dsTa, this.aliasGenerator, this.conn, this.lockId );
941            deleteHandler.deleteOrphanedPropertyRows( relation, keyValues );
942        }
943    
944        private void updateProperty( @SuppressWarnings("unused")
945        FeatureId fid, @SuppressWarnings("unused")
946        MappedFeatureType ft, @SuppressWarnings("unused")
947        MappedFeaturePropertyType pt, @SuppressWarnings("unused")
948        Feature replacementFeature ) {
949            throw new UnsupportedOperationException( "Updating of feature properties is not implemented yet." );
950        }
951    
952        private void appendFIDWhereCondition( StatementBuffer query, FeatureId fid ) {
953            MappingField[] fidFields = fid.getFidDefinition().getIdFields();
954            for ( int i = 0; i < fidFields.length; i++ ) {
955                query.append( fidFields[i].getField() );
956                query.append( "=?" );
957                query.addArgument( fid.getValue( i ), fidFields[i].getType() );
958                if ( i != fidFields.length - 1 ) {
959                    query.append( " AND " );
960                }
961            }
962        }
963    }