001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/io/datastore/sde/SDEInsertHandler.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2006 by: M.O.S.S. Computer Grafik Systeme GmbH
006     Hohenbrunner Weg 13
007     D-82024 Taufkirchen
008     http://www.moss.de/
009    
010     This library is free software; you can redistribute it and/or
011     modify it under the terms of the GNU Lesser General Public
012     License as published by the Free Software Foundation; either
013     version 2.1 of the License, or (at your option) any later version.
014    
015     This library is distributed in the hope that it will be useful,
016     but WITHOUT ANY WARRANTY; without even the implied warranty of
017     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
018     Lesser General Public License for more details.
019    
020     You should have received a copy of the GNU Lesser General Public
021     License along with this library; if not, write to the Free Software
022     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
023    
024     ---------------------------------------------------------------------------*/
025    package org.deegree.io.datastore.sde;
026    
027    import java.util.ArrayList;
028    import java.util.Collection;
029    import java.util.HashMap;
030    import java.util.Iterator;
031    import java.util.List;
032    import java.util.Map;
033    
034    import org.deegree.datatypes.Types;
035    import org.deegree.framework.log.ILogger;
036    import org.deegree.framework.log.LoggerFactory;
037    import org.deegree.framework.util.StringTools;
038    import org.deegree.io.datastore.DatastoreException;
039    import org.deegree.io.datastore.FeatureId;
040    import org.deegree.io.datastore.TransactionException;
041    import org.deegree.io.datastore.idgenerator.FeatureIdAssigner;
042    import org.deegree.io.datastore.idgenerator.IdGenerationException;
043    import org.deegree.io.datastore.idgenerator.ParentIDGenerator;
044    import org.deegree.io.datastore.schema.MappedFeaturePropertyType;
045    import org.deegree.io.datastore.schema.MappedFeatureType;
046    import org.deegree.io.datastore.schema.MappedGeometryPropertyType;
047    import org.deegree.io.datastore.schema.MappedPropertyType;
048    import org.deegree.io.datastore.schema.MappedSimplePropertyType;
049    import org.deegree.io.datastore.schema.TableRelation;
050    import org.deegree.io.datastore.schema.content.MappingField;
051    import org.deegree.io.datastore.schema.content.SimpleContent;
052    import org.deegree.io.datastore.sql.transaction.insert.FeatureRow;
053    import org.deegree.io.datastore.sql.transaction.insert.InsertField;
054    import org.deegree.io.datastore.sql.transaction.insert.InsertRow;
055    import org.deegree.io.sdeapi.SDEAdapter;
056    import org.deegree.model.feature.Feature;
057    import org.deegree.model.feature.FeatureProperty;
058    import org.deegree.model.feature.schema.FeaturePropertyType;
059    import org.deegree.model.feature.schema.GeometryPropertyType;
060    import org.deegree.model.feature.schema.SimplePropertyType;
061    import org.deegree.model.spatialschema.Geometry;
062    
063    import com.esri.sde.sdk.client.SeException;
064    import com.esri.sde.sdk.client.SeInsert;
065    import com.esri.sde.sdk.client.SeObjectId;
066    import com.esri.sde.sdk.client.SeQuery;
067    import com.esri.sde.sdk.client.SeRow;
068    import com.esri.sde.sdk.client.SeSqlConstruct;
069    import com.esri.sde.sdk.client.SeState;
070    
071    /**
072     * Handler for <code>Insert</code> operations (usually contained in <code>Transaction</code>
073     * requests).
074     * 
075     * @author <a href="mailto:cpollmann@moss.de">Christoph Pollmann</a>
076     * @author last edited by: $Author: apoth $
077     * 
078     * @version $Revision: 7844 $
079     */
080    public class SDEInsertHandler extends AbstractSDERequestHandler {
081    
082        private static final ILogger LOG = LoggerFactory.getLogger( SDEInsertHandler.class );
083    
084        // features that are currently being processed
085        private Map<FeatureId, FeatureRow> featuresInInsertion = new HashMap<FeatureId, FeatureRow>();
086    
087        // contains only property rows and join table rows (but no feature rows)
088        private List<InsertRow> insertRows = new ArrayList<InsertRow>();
089    
090        private SDETransaction dsTa;
091    
092        /**
093         * Creates a new <code>InsertHandler</code> from the given parameters.
094         * 
095         * @param dsTa
096         */
097        public SDEInsertHandler( SDETransaction dsTa ) {
098            super( dsTa.getDatastore(), dsTa.getAliasGenerator(), dsTa.getConnection() );
099            this.dsTa = dsTa;
100        }
101    
102        /**
103         * Inserts the given feature instance into the datastore.
104         * 
105         * @param features
106         *            (which have a MappedFeatureType as feature type)
107         * @return feature ids of inserted (root) feature instances
108         * @throws DatastoreException
109         *             if the insert could not be performed
110         */
111        public List<FeatureId> performInsert( List<Feature> features )
112                                throws DatastoreException {
113    
114            List<FeatureId> fids = new ArrayList<FeatureId>();
115    
116            for ( int i = 0; i < features.size(); i++ ) {
117                Feature feature = features.get( i );
118                MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType();
119                if ( feature.getId().startsWith( FeatureIdAssigner.EXISTS_MARKER ) ) {
120                    String msg = "feature already exists " + feature.getName() + " id=" + feature.getId().substring( 1 );
121                    throw new TransactionException( msg );
122                }
123                LOG.logDebug( "Inserting root feature '" + feature.getId() + "'..." );
124                insertFeature( feature );
125                FeatureId fid = new FeatureId( ft, feature.getId() );
126                fids.add( fid );
127            }
128    
129            // merge inserts rows that are identical (except their pks)
130            this.insertRows = mergeInsertRows( this.insertRows );
131    
132            // add featureRows to insertRows
133            Iterator<FeatureRow> iter = this.featuresInInsertion.values().iterator();
134            while ( iter.hasNext() ) {
135                this.insertRows.add( iter.next() );
136            }
137    
138            // check for cyclic fk constraints
139            Collection<InsertRow> cycle = InsertRow.findCycle( this.insertRows );
140            if ( cycle != null ) {
141                Iterator<InsertRow> cycleIter = cycle.iterator();
142                StringBuffer sb = new StringBuffer();
143                while ( cycleIter.hasNext() ) {
144                    sb.append( cycleIter.next() );
145                    if ( cycle.iterator().hasNext() ) {
146                        sb.append( " -> " );
147                    }
148                }
149                String msg = "ERROR_FK_CYCLE " + sb.toString();
150                throw new TransactionException( msg );
151            }
152    
153            // sort the insert rows topologically
154            List<InsertRow> sortedInserts = InsertRow.sortInsertRows( this.insertRows );
155    
156            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
157                Iterator<InsertRow> iter2 = sortedInserts.iterator();
158                LOG.logDebug( sortedInserts.size() + " rows to be inserted: " );
159                while ( iter2.hasNext() ) {
160                    LOG.logDebug( iter2.next().toString() );
161                }
162            }
163    
164            executeInserts( sortedInserts );
165    
166            return fids;
167        }
168    
169        /**
170         * Builds the <code>InsertRows</code> that are necessary to insert the given feature instance
171         * (including all properties + subfeatures).
172         * 
173         * @param feature
174         * @return
175         * @throws TransactionException
176         */
177        private FeatureRow insertFeature( Feature feature )
178                                throws TransactionException {
179    
180            MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType();
181            if ( !ft.isInsertable() ) {
182                String msg = "featuretype can't be inserted " + ft.getName();
183                throw new TransactionException( msg );
184            }
185    
186            LOG.logDebug( "Creating InsertRow for feature with type '" + ft.getName() + "' and id: '" + feature.getId()
187                          + "'." );
188    
189            // extract feature id column value
190            MappingField[] fidFields = ft.getGMLId().getIdFields();
191            if ( fidFields.length > 1 ) {
192                throw new TransactionException( "Insertion of features with compound feature ids is not " + "supported." );
193            }
194            Object fidValue = null;
195            try {
196                fidValue = FeatureId.removeFIDPrefix( feature.getId(), ft.getGMLId() );
197            } catch ( DatastoreException e ) {
198                e.printStackTrace();
199                throw new TransactionException( e.getMessage(), e );
200            }
201            FeatureId fid = new FeatureId( ft, new Object[] { fidValue } );
202    
203            // check if the feature id is already being inserted (happens for cyclic features)
204            FeatureRow insertRow = this.featuresInInsertion.get( fid );
205            if ( insertRow != null ) {
206                return insertRow;
207            }
208    
209            insertRow = new FeatureRow( ft.getTable() );
210            this.featuresInInsertion.put( fid, insertRow );
211    
212            // add column value for fid (primary key)
213            String fidColumn = fidFields[0].getField();
214            insertRow.setColumn( fidColumn, fidValue, ft.getGMLId().getIdFields()[0].getType(), true );
215    
216            // process properties
217            FeatureProperty[] properties = feature.getProperties();
218            for ( int i = 0; i < properties.length; i++ ) {
219                FeatureProperty property = properties[i];
220                MappedPropertyType propertyType = (MappedPropertyType) ft.getProperty( property.getName() );
221                if ( propertyType == null ) {
222                    String msg = "Unknown propertytype " + property.getName();
223                    LOG.logDebug( msg );
224                    throw new TransactionException( msg );
225                }
226                insertProperty( property, propertyType, insertRow );
227            }
228            return insertRow;
229        }
230    
231        /**
232         * Builds the <code>InsertRow</code>s that are necessary to insert the given property
233         * instance (including all it's subfeatures).
234         * 
235         * @param property
236         *            property instance to be inserted
237         * @param propertyType
238         *            property type of the property
239         * @param featureRow
240         *            table row of the parent feature instance
241         * @throws TransactionException
242         */
243        private void insertProperty( FeatureProperty property, MappedPropertyType propertyType, InsertRow featureRow )
244                                throws TransactionException {
245    
246            if ( propertyType instanceof SimplePropertyType ) {
247                String msg = StringTools.concat( 300, "- Simple property '", propertyType.getName(),
248                                                 "', value='" + getPropertyValue( property ), "'." );
249                LOG.logDebug( msg );
250                insertProperty( (MappedSimplePropertyType) propertyType, property, featureRow );
251            } else if ( propertyType instanceof GeometryPropertyType ) {
252                LOG.logDebug( "- Geometry property: '" + propertyType.getName() + "'" );
253                insertProperty( (MappedGeometryPropertyType) propertyType, property, featureRow );
254            } else if ( propertyType instanceof FeaturePropertyType ) {
255                LOG.logDebug( "- Feature property: '" + propertyType.getName() + "'" );
256                insertProperty( (MappedFeaturePropertyType) propertyType, property, featureRow );
257            } else {
258                throw new TransactionException( "Unhandled property type '" + propertyType.getClass().getName() + "'." );
259            }
260        }
261    
262        /**
263         * Inserts the given simple property (stored in feature table or in related table).
264         * 
265         * @param pt
266         * @param property
267         * @param featureRow
268         * @throws TransactionException
269         */
270        private void insertProperty( MappedSimplePropertyType pt, FeatureProperty property, InsertRow featureRow )
271                                throws TransactionException {
272    
273            SimpleContent content = pt.getContent();
274            if ( content.isUpdateable() ) {
275                if ( content instanceof MappingField ) {
276                    MappingField mf = (MappingField) content;
277                    String propertyColumn = mf.getField();
278                    Object propertyValue = property.getValue();
279                    int propertyType = mf.getType();
280                    TableRelation[] relations = pt.getTableRelations();
281                    insertProperty( propertyColumn, propertyValue, propertyType, relations, featureRow );
282                }
283            }
284        }
285    
286        /**
287         * Inserts the given geometry property (stored in feature table or in related table).
288         * 
289         * @param pt
290         * @param property
291         * @param featureRow
292         * @throws TransactionException
293         */
294        private void insertProperty( MappedGeometryPropertyType pt, FeatureProperty property, InsertRow featureRow )
295                                throws TransactionException {
296    
297            String propertyColumn = pt.getMappingField().getField();
298    
299            Geometry deegreeGeometry = (Geometry) property.getValue();
300            Object dbGeometry;
301    
302            try {
303                dbGeometry = this.datastore.convertDegreeToDBGeometry( deegreeGeometry );
304            } catch ( DatastoreException e ) {
305                throw new TransactionException( e.getMessage(), e );
306            }
307    
308            int propertyType = pt.getMappingField().getType();
309    
310            TableRelation[] relations = pt.getTableRelations();
311            insertProperty( propertyColumn, dbGeometry, propertyType, relations, featureRow );
312        }
313    
314        /**
315         * Inserts the given simple / geometry property (stored in feature table or in related table).
316         * 
317         * @param propertyColumn
318         * @param propertyValue
319         * @param propertyType
320         * @param featureRow
321         * @throws TransactionException
322         */
323        private void insertProperty( String propertyColumn, Object propertyValue, int propertyType,
324                                     TableRelation[] relations, InsertRow featureRow )
325                                throws TransactionException {
326    
327            if ( relations == null || relations.length == 0 ) {
328                // property is stored in feature table
329                featureRow.setColumn( propertyColumn, propertyValue, propertyType, false );
330            } else {
331                // property is stored in related table
332                if ( relations.length > 1 ) {
333                    throw new TransactionException( "properties in related tables are not allowed here" );
334                }
335    
336                if ( !relations[0].isFromFK() ) {
337                    // fk is in property table
338                    MappingField[] pkFields = relations[0].getFromFields();
339                    MappingField[] fkFields = relations[0].getToFields();
340    
341                    for ( int i = 0; i < pkFields.length; i++ ) {
342                        InsertField pkField = featureRow.getColumn( pkFields[i].getField() );
343                        if ( pkField == null ) {
344                            String msg = "Missing foreign key " + pkFields[i].getField() + " / " + pkFields[i].getTable();
345                            throw new TransactionException( msg );
346                        }
347                        int pkColumnType = pkField.getSQLType();
348                        int fkColumnType = fkFields[i].getType();
349                        if ( pkColumnType != fkColumnType ) {
350                            String msg = "FK_PK_TYPE_MISMATCH";
351                            throw new TransactionException( msg );
352                        }
353                        InsertRow insertRow = new InsertRow( relations[0].getToTable() );
354                        insertRow.linkColumn( fkFields[i].getField(), pkField );
355                        insertRow.setColumn( propertyColumn, propertyValue, propertyType, false );
356                        this.insertRows.add( insertRow );
357                    }
358                } else {
359                    // fk is in feature table
360                    MappingField[] pkFields = relations[0].getToFields();
361                    MappingField[] fkFields = relations[0].getFromFields();
362    
363                    // generate necessary primary key value
364                    InsertField pkField = null;
365                    try {
366                        Object pk = null;
367                        // TODO remove hack!!!
368                        if ( relations[0].getIdGenerator() instanceof ParentIDGenerator ) {
369                            InsertField field = featureRow.getColumn( "ID" );
370                            if ( field == null ) {
371                                throw new TransactionException( "No value for ID available!" );
372                            }
373                            pk = field.getValue();
374                        } else {
375                            pk = relations[0].getNewPK( this.dsTa );
376                        }
377                        InsertRow insertRow = findOrCreateRow( relations[0].getToTable(), pkFields[0].getField(), pk );
378                        pkField = insertRow.setColumn( pkFields[0].getField(), pk, pkFields[0].getType(), true );
379                        insertRow.setColumn( propertyColumn, propertyValue, propertyType, false );
380                    } catch ( IdGenerationException e ) {
381                        throw new TransactionException( e.getMessage(), e );
382                    }
383                    featureRow.linkColumn( fkFields[0].getField(), pkField );
384                }
385            }
386        }
387    
388        /**
389         * Inserts the given feature property.
390         * 
391         * @param pt
392         * @param property
393         * @param featureRow
394         * @throws TransactionException
395         */
396        private void insertProperty( MappedFeaturePropertyType pt, FeatureProperty property, InsertRow featureRow )
397                                throws TransactionException {
398    
399            // find (concrete) subfeature type for the given property instance
400            MappedFeatureType propertyFeatureType = pt.getFeatureTypeReference().getFeatureType();
401            MappedFeatureType[] substitutions = propertyFeatureType.getConcreteSubstitutions();
402            Feature subFeature = (Feature) property.getValue();
403            MappedFeatureType subFeatureType = null;
404            for ( int i = 0; i < substitutions.length; i++ ) {
405                if ( substitutions[i].getName().equals( subFeature.getName() ) ) {
406                    subFeatureType = substitutions[i];
407                    break;
408                }
409            }
410            if ( subFeatureType == null ) {
411                String msg = "ERROR_FEATURE_NOT_SUBSTITUTABLE " + propertyFeatureType.getName() + "->"
412                             + subFeature.getName();
413                throw new TransactionException( msg );
414            }
415            boolean ftIsAbstract = propertyFeatureType.isAbstract();
416    
417            TableRelation[] relations = pt.getTableRelations();
418            if ( relations == null || relations.length < 1 ) {
419                throw new TransactionException( "Invalid feature property definition, feature property "
420                                                + "mappings must use at least one 'TableRelation' element." );
421            }
422    
423            // workaround for links to dummy InsertRows (of already stored features)
424            boolean cutLink = subFeature.getId().startsWith( FeatureIdAssigner.EXISTS_MARKER );
425            InsertRow subFeatureRow = null;
426            if ( cutLink ) {
427                try {
428                    Object fidValue = FeatureId.removeFIDPrefix( subFeature.getId().substring( 1 ),
429                                                                 subFeatureType.getGMLId() );
430                    subFeatureRow = new FeatureRow( subFeatureType.getTable() );
431                    // add column value for fid (primary key)
432                    String fidColumn = subFeatureType.getGMLId().getIdFields()[0].getField();
433                    subFeatureRow.setColumn( fidColumn, fidValue, subFeatureType.getGMLId().getIdFields()[0].getType(),
434                                             true );
435                } catch ( DatastoreException e ) {
436                    throw new TransactionException( e );
437                }
438            } else {
439                // insert sub feature (if it is not already stored)
440                subFeatureRow = insertFeature( subFeature );
441            }
442    
443            if ( relations.length == 1 ) {
444    
445                if ( relations[0].isFromFK() ) {
446                    // fk is in feature table
447                    MappingField[] pkFields = relations[0].getToFields();
448                    MappingField[] fkFields = relations[0].getFromFields();
449    
450                    for ( int i = 0; i < pkFields.length; i++ ) {
451                        InsertField pkField = subFeatureRow.getColumn( pkFields[i].getField() );
452                        if ( pkField == null ) {
453                            String msg = "Missing foreign key " + pkField.getColumnName() + " / " + pkField.getTable();
454                            throw new TransactionException( msg );
455                        }
456                        int pkColumnType = pkField.getSQLType();
457                        int fkColumnType = fkFields[i].getType();
458                        if ( pkColumnType != fkColumnType ) {
459                            String msg = "ERROR_FK_PK_TYPE_MISMATCH";
460                            throw new TransactionException( msg );
461                        }
462    
463                        if ( !cutLink ) {
464                            featureRow.linkColumn( fkFields[i].getField(), pkField );
465                        } else {
466                            featureRow.setColumn( fkFields[i].getField(), pkField.getValue(), pkField.getSQLType(), false );
467                        }
468    
469                    }
470    
471                    if ( ftIsAbstract ) {
472                        String typeField = FT_PREFIX + relations[0].getToTable();
473                        featureRow.setColumn( typeField, subFeatureType.getName().getLocalName(), Types.VARCHAR, false );
474                    }
475                } else {
476                    // fk is in subfeature table
477                    MappingField[] pkFields = relations[0].getFromFields();
478                    MappingField[] fkFields = relations[0].getToFields();
479    
480                    InsertField pkField = featureRow.getColumn( pkFields[0].getField() );
481    
482                    if ( pkField == null ) {
483                        String msg = "Missing foreign key " + pkField.getColumnName() + " / " + pkField.getTable();
484                        throw new TransactionException( msg );
485                    }
486                    int pkColumnType = pkField.getSQLType();
487                    int fkColumnType = fkFields[0].getType();
488                    if ( pkColumnType != fkColumnType ) {
489                        String msg = "ERROR_FK_PK_TYPE_MISMATCH";
490                        throw new TransactionException( msg );
491                    }
492    
493                    if ( !cutLink ) {
494                        subFeatureRow.linkColumn( fkFields[0].getField(), pkField );
495                    } else {
496                        featureRow.setColumn( fkFields[0].getField(), pkField.getValue(), pkField.getSQLType(), false );
497                    }
498                }
499            } else if ( relations.length == 2 ) {
500    
501                // insert into join table
502                String joinTable = relations[0].getToTable();
503                MappingField[] leftKeyFields = relations[0].getToFields();
504                MappingField[] rightKeyFields = relations[1].getFromFields();
505    
506                InsertRow jtRow = new InsertRow( joinTable );
507                if ( ftIsAbstract ) {
508                    jtRow.setColumn( FT_COLUMN, subFeatureType.getName().getLocalName(), Types.VARCHAR, false );
509                }
510    
511                if ( !relations[0].isFromFK() ) {
512                    // left key field in join table is fk
513                    MappingField[] pkFields = relations[0].getFromFields();
514                    InsertField pkField = featureRow.getColumn( pkFields[0].getField() );
515                    if ( pkField == null ) {
516                        throw new TransactionException( "Insertion of feature property using join table failed: "
517                                                        + "no value for join table key column '" + pkField.getColumnName()
518                                                        + "'." );
519                    }
520                    jtRow.linkColumn( leftKeyFields[0].getField(), pkField );
521                } else {
522                    // left key field in join table is pk
523                    MappingField[] pkFields = relations[0].getToFields();
524                    // generate necessary primary key value
525                    InsertField pkField = null;
526                    try {
527                        Object pk = relations[0].getNewPK( this.dsTa );
528                        pkField = jtRow.setColumn( pkFields[0].getField(), pk, pkFields[0].getType(), true );
529                    } catch ( IdGenerationException e ) {
530                        throw new TransactionException( e.getMessage(), e );
531                    }
532                    featureRow.linkColumn( relations[0].getFromFields()[0].getField(), pkField );
533                }
534    
535                if ( relations[1].isFromFK() ) {
536                    // right key field in join table is fk
537                    MappingField[] pkFields = relations[1].getToFields();
538                    InsertField pkField = subFeatureRow.getColumn( pkFields[0].getField() );
539                    if ( pkField == null ) {
540                        throw new TransactionException( "Insertion of feature property using join table failed: "
541                                                        + "no value for join table key column '" + pkField.getColumnName()
542                                                        + "'." );
543                    }
544                    if ( !cutLink ) {
545                        jtRow.linkColumn( rightKeyFields[0].getField(), pkField );
546                    } else {
547                        jtRow.setColumn( rightKeyFields[0].getField(), pkField.getValue(), pkField.getSQLType(), false );
548                    }
549                } else {
550                    // right key field in join table is pk
551                    MappingField[] pkFields = relations[1].getFromFields();
552                    // generate necessary primary key value
553                    InsertField pkField = null;
554                    try {
555                        Object pk = relations[1].getNewPK( this.dsTa );
556                        pkField = jtRow.setColumn( pkFields[0].getField(), pk, pkFields[0].getType(), true );
557                    } catch ( IdGenerationException e ) {
558                        throw new TransactionException( e.getMessage(), e );
559                    }
560                    if ( !cutLink ) {
561                        subFeatureRow.linkColumn( relations[1].getToFields()[0].getField(), pkField );
562                    }
563                }
564    
565                this.insertRows.add( jtRow );
566            } else {
567                throw new TransactionException( "Insertion of feature properties stored in related tables "
568                                                + "connected via more than one join table is not supported." );
569            }
570        }
571    
572        /**
573         * Checks whether the feature that corresponds to the given FeatureRow is already stored in the
574         * database.
575         * 
576         * @param featureRow
577         * @return
578         * @throws DatastoreException
579         */
580        private boolean doesFeatureExist( FeatureRow featureRow )
581                                throws TransactionException {
582    
583            boolean exists = false;
584    
585            InsertField pkField = featureRow.getPKColumn();
586    
587            SeQuery stmt = null;
588            try {
589                stmt = buildFeatureSelect( pkField.getColumnName(), pkField.getValue(), featureRow.getTable() );
590                stmt.execute();
591                SeRow row = stmt.fetch();
592                if ( null != row ) {
593                    exists = true;
594                }
595                row = stmt.fetch();
596                if ( null != row ) {
597                    String msg = "ERROR_FEATURE_QUERY_MORE_THAN_ONE_RESULT";
598                    LOG.logError( msg );
599                    throw new TransactionException( msg );
600                }
601            } catch ( Exception e ) {
602                throw new TransactionException( e );
603            } finally {
604                try {
605                    stmt.close();
606                } catch ( Exception e ) {
607                    LOG.logDebug( "Error in SDE command", e );
608                }
609            }
610            return exists;
611        }
612    
613        /**
614         * Builds a SELECT statement that checks for the existence of a feature with the given id.
615         * 
616         * @param fidColumn
617         * @param typeCode
618         * @param fidValue
619         * @param table
620         * @return the statement
621         */
622        private SeQuery buildFeatureSelect( String fidColumn, Object fidValue, String table ) {
623            SeQuery query = null;
624            try {
625                SeSqlConstruct constr = new SeSqlConstruct( table, fidColumn + "='" + fidValue.toString() + "'" );
626                String[] columns = new String[1];
627                columns[0] = fidColumn;
628                query = new SeQuery( getConnection().getConnection(), columns, constr );
629                query.setState( getConnection().getState().getId(), new SeObjectId( SeState.SE_NULL_STATE_ID ),
630                                SeState.SE_STATE_DIFF_NOCHECK );
631                query.prepareQuery();
632            } catch ( Exception e ) {
633                LOG.logError( "Error building featureSelect", e );
634            }
635            return query;
636        }
637    
638        private InsertRow findOrCreateRow( String table, String pkColumn, Object value ) {
639            Iterator<InsertRow> rowIter = this.insertRows.iterator();
640            boolean found = false;
641            InsertRow row = null;
642            while ( rowIter.hasNext() ) {
643                row = rowIter.next();
644                if ( row.getTable().equals( table ) ) {
645                    InsertField field = row.getColumn( pkColumn );
646                    if ( value.equals( field.getValue() ) ) {
647                        found = true;
648                        LOG.logDebug( "Found matching row " + row );
649                        break;
650                    }
651                }
652            }
653            if ( !found ) {
654                row = new InsertRow( table );
655                this.insertRows.add( row );
656            }
657            return row;
658        }
659    
660        private String getPropertyValue( FeatureProperty property ) {
661            Object value = property.getValue();
662            StringBuffer sb = new StringBuffer();
663            if ( value instanceof Object[] ) {
664                Object[] objects = (Object[]) value;
665                for ( int i = 0; i < objects.length; i++ ) {
666                    sb.append( objects[i] );
667                }
668            } else {
669                sb.append( value );
670            }
671            return sb.toString();
672        }
673    
674        /**
675         * Transforms the given <code>List</code> of <code>InsertRows</code> into SQL INSERT
676         * statements and executes them using the underlying JDBC connection.
677         * 
678         * @param inserts
679         * @throws TransactionException
680         *             if an SQL error occurs
681         */
682        private void executeInserts( List<InsertRow> inserts )
683                                throws TransactionException {
684    
685            SeInsert stmt = null;
686    
687            for ( InsertRow row : inserts ) {
688                if ( row instanceof FeatureRow ) {
689                    if ( doesFeatureExist( (FeatureRow) row ) ) {
690                        LOG.logDebug( "Skipping feature row. Already present in db." );
691                        continue;
692                    }
693                }
694                try {
695                    stmt = createStatement( row );
696                    stmt.execute();
697                } catch ( Exception e ) {
698                    String msg = "Error performing insert: " + e.getMessage();
699                    LOG.logError( msg, e );
700                    throw new TransactionException( msg, e );
701                } finally {
702                    if ( stmt != null ) {
703                        try {
704                            stmt.close();
705                        } catch ( Exception e ) {
706                            String msg = "Error closing statement: " + e.getMessage();
707                            LOG.logError( msg, e );
708                        }
709                    }
710                }
711            }
712        }
713    
714        private SeInsert createStatement( InsertRow row )
715                                throws SeException {
716            SeInsert inserter = new SeInsert( conn.getConnection() );
717            inserter.setState( conn.getState().getId(), new SeObjectId( SeState.SE_NULL_STATE_ID ),
718                               SeState.SE_STATE_DIFF_NOCHECK );
719            Collection<InsertField> fields = row.getColumns();
720            String[] columns = new String[fields.size()];
721            int i = 0;
722            for ( Iterator<InsertField> iter = fields.iterator(); iter.hasNext(); i++ ) {
723                InsertField field = iter.next();
724                columns[i] = field.getColumnName();
725            }
726            inserter.intoTable( row.getTable(), columns );
727            SeRow insertRow = inserter.getRowToSet();
728            for ( i = 0; i < columns.length; i++ ) {
729                InsertField field = row.getColumn( columns[i] );
730                SDEAdapter.setRowValue( insertRow, i, field.getValue(), SDEAdapter.mapSQL2SDE( field.getSQLType() ) );
731            }
732            return inserter;
733        }
734    
735        /**
736         * Merges the given <code>InsertRow</code>s by eliminating rows that have identical content
737         * (except for their primary keys).
738         * <p>
739         * This only applies to non-FeatureRows: there are never two FeatureRows that may be treated as
740         * identical, because unique feature ids have been assigned to them before.
741         * 
742         * @see FeatureIdAssigner
743         * 
744         * @param insertRows
745         * @return
746         */
747        private List<InsertRow> mergeInsertRows( List<InsertRow> insertRows ) {
748    
749            List<InsertRow> result = new ArrayList<InsertRow>();
750    
751            // keys: table names, values: inserts into the table
752            Map<String, Collection<InsertRow>> tableMap = new HashMap<String, Collection<InsertRow>>();
753    
754            // build table lookup map
755            Iterator<InsertRow> iter = insertRows.iterator();
756            while ( iter.hasNext() ) {
757                InsertRow insertRow = iter.next();
758                Collection<InsertRow> tableInserts = tableMap.get( insertRow.getTable() );
759                if ( tableInserts == null ) {
760                    tableInserts = new ArrayList<InsertRow>();
761                    tableMap.put( insertRow.getTable(), tableInserts );
762                }
763                tableInserts.add( insertRow );
764            }
765    
766            iter = insertRows.iterator();
767            while ( iter.hasNext() ) {
768                InsertRow insertRow = iter.next();
769                boolean insert = true;
770                if ( !( insertRow instanceof FeatureRow ) ) {
771                    Collection<InsertRow> tableInserts = tableMap.get( insertRow.getTable() );
772                    Iterator<InsertRow> candidatesIter = tableInserts.iterator();
773                    while ( candidatesIter.hasNext() ) {
774                        InsertRow candidate = candidatesIter.next();
775                        if ( insertRow != candidate ) {
776                            if ( compareInsertRows( insertRow, candidate ) ) {
777                                LOG.logDebug( "Removing InsertRow: " + insertRow.hashCode() + " " + insertRow
778                                              + " - duplicate of: " + candidate );
779                                replaceInsertRow( insertRow, candidate );
780                                insert = false;
781                                tableInserts.remove( insertRow );
782                                break;
783                            }
784                        }
785                    }
786                }
787                if ( insert ) {
788                    result.add( insertRow );
789                }
790            }
791            return result;
792        }
793    
794        private boolean compareInsertRows( InsertRow row1, InsertRow row2 ) {
795            Collection<InsertField> fields1 = row1.getColumns();
796            Iterator<InsertField> iter = fields1.iterator();
797            while ( iter.hasNext() ) {
798                InsertField field1 = iter.next();
799                if ( !field1.isPK() ) {
800                    InsertField field2 = row2.getColumn( field1.getColumnName() );
801                    Object value1 = field1.getValue();
802                    Object value2 = null;
803                    if ( field2 != null )
804                        value2 = field2.getValue();
805                    if ( value1 == null ) {
806                        if ( value2 == null ) {
807                            continue;
808                        }
809                        return false;
810                    }
811                    if ( !value1.equals( value2 ) ) {
812                        return false;
813                    }
814                }
815            }
816            return true;
817        }
818    
819        private void replaceInsertRow( InsertRow oldRow, InsertRow newRow ) {
820    
821            Collection<InsertField> oldFields = oldRow.getColumns();
822            for ( InsertField field : oldFields ) {
823                InsertField toField = field.getReferencedField();
824                if ( toField != null ) {
825                    LOG.logDebug( "Removing reference to field '" + toField + "'" );
826                    toField.removeReferencingField( field );
827                }
828            }
829    
830            Collection<InsertField> referencingFields = oldRow.getReferencingFields();
831            for ( InsertField fromField : referencingFields ) {
832                LOG.logDebug( "Replacing reference for field '" + fromField + "'" );
833                InsertField field = newRow.getColumn( fromField.getReferencedField().getColumnName() );
834                LOG.logDebug( "" + field );
835                fromField.relinkField( field );
836            }
837        }
838    }