    001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.4_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    }