001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/io/datastore/sql/transaction/insert/InsertHandler.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2008 by:
006     EXSE, Department of Geography, University of Bonn
007     http://www.giub.uni-bonn.de/deegree/
008     lat/lon GmbH
009     http://www.lat-lon.de
010    
011     This library is free software; you can redistribute it and/or
012     modify it under the terms of the GNU Lesser General Public
013     License as published by the Free Software Foundation; either
014     version 2.1 of the License, or (at your option) any later version.
015    
016     This library is distributed in the hope that it will be useful,
017     but WITHOUT ANY WARRANTY; without even the implied warranty of
018     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
019     Lesser General Public License for more details.
020    
021     You should have received a copy of the GNU Lesser General Public
022     License along with this library; if not, write to the Free Software
023     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
024    
025     Contact:
026    
027     Andreas Poth
028     lat/lon GmbH
029     Aennchenstraße 19
030     53177 Bonn
031     Germany
032     E-Mail: poth@lat-lon.de
033    
034     Prof. Dr. Klaus Greve
035     Department of Geography
036     University of Bonn
037     Meckenheimer Allee 166
038     53115 Bonn
039     Germany
040     E-Mail: greve@giub.uni-bonn.de
041    
042     ---------------------------------------------------------------------------*/
043    package org.deegree.io.datastore.sql.transaction.insert;
044    
045    import java.sql.Connection;
046    import java.sql.PreparedStatement;
047    import java.sql.ResultSet;
048    import java.sql.SQLException;
049    import java.util.ArrayList;
050    import java.util.Collection;
051    import java.util.HashMap;
052    import java.util.Iterator;
053    import java.util.List;
054    import java.util.Map;
055    
056    import org.deegree.datatypes.Types;
057    import org.deegree.datatypes.UnknownTypeException;
058    import org.deegree.framework.log.ILogger;
059    import org.deegree.framework.log.LoggerFactory;
060    import org.deegree.i18n.Messages;
061    import org.deegree.io.datastore.DatastoreException;
062    import org.deegree.io.datastore.FeatureId;
063    import org.deegree.io.datastore.TransactionException;
064    import org.deegree.io.datastore.idgenerator.FeatureIdAssigner;
065    import org.deegree.io.datastore.idgenerator.IdGenerationException;
066    import org.deegree.io.datastore.idgenerator.ParentIDGenerator;
067    import org.deegree.io.datastore.schema.MappedFeaturePropertyType;
068    import org.deegree.io.datastore.schema.MappedFeatureType;
069    import org.deegree.io.datastore.schema.MappedGeometryPropertyType;
070    import org.deegree.io.datastore.schema.MappedPropertyType;
071    import org.deegree.io.datastore.schema.MappedSimplePropertyType;
072    import org.deegree.io.datastore.schema.TableRelation;
073    import org.deegree.io.datastore.schema.content.MappingField;
074    import org.deegree.io.datastore.schema.content.MappingGeometryField;
075    import org.deegree.io.datastore.schema.content.SimpleContent;
076    import org.deegree.io.datastore.sql.AbstractRequestHandler;
077    import org.deegree.io.datastore.sql.StatementBuffer;
078    import org.deegree.io.datastore.sql.TableAliasGenerator;
079    import org.deegree.io.datastore.sql.transaction.SQLTransaction;
080    import org.deegree.model.feature.Feature;
081    import org.deegree.model.feature.FeatureProperty;
082    import org.deegree.model.feature.schema.FeaturePropertyType;
083    import org.deegree.model.feature.schema.GeometryPropertyType;
084    import org.deegree.model.feature.schema.SimplePropertyType;
085    import org.deegree.model.spatialschema.Geometry;
086    import org.deegree.ogcwebservices.wfs.operation.transaction.Insert;
087    import org.deegree.ogcwebservices.wfs.operation.transaction.Transaction;
088    
089    /**
090     * Handler for {@link Insert} operations (usually contained in {@link Transaction} requests).
091     * 
092     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
093     * @author last edited by: $Author: apoth $
094     * 
095     * @version $Revision: 9342 $, $Date: 2007-12-27 13:32:57 +0100 (Do, 27 Dez 2007) $
096     */
097    public class InsertHandler extends AbstractRequestHandler {
098    
099        private static final ILogger LOG = LoggerFactory.getLogger( InsertHandler.class );
100    
101        // features that are currently being processed
102        private Map<FeatureId, FeatureRow> featuresInInsertion = new HashMap<FeatureId, FeatureRow>();
103    
104        // contains only property rows and join table rows (but no feature rows)
105        private List<InsertRow> insertRows = new ArrayList<InsertRow>();
106    
107        private SQLTransaction dsTa;
108    
109        /**
110         * Creates a new <code>InsertHandler</code> from the given parameters.
111         * 
112         * @param dsTa
113         * @param aliasGenerator
114         * @param conn
115         */
116        public InsertHandler( SQLTransaction dsTa, TableAliasGenerator aliasGenerator, Connection conn ) {
117            super( dsTa.getDatastore(), aliasGenerator, conn );
118            this.dsTa = dsTa;
119        }
120    
121        /**
122         * Inserts the given feature instance into the datastore.
123         * 
124         * @param features
125         *            (which have a MappedFeatureType as feature type)
126         * @return feature ids of inserted (root) feature instances
127         * @throws DatastoreException
128         *             if the insert could not be performed
129         */
130        public List<FeatureId> performInsert( List<Feature> features )
131                                throws DatastoreException {
132    
133            List<FeatureId> fids = new ArrayList<FeatureId>();
134            for ( int i = 0; i < features.size(); i++ ) {
135                Feature feature = features.get( i );
136    
137                MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType();
138                if ( feature.getId().startsWith( FeatureIdAssigner.EXISTS_MARKER ) ) {
139                    String msg = Messages.getMessage( "DATASTORE_FEATURE_EXISTS", feature.getName(),
140                                                      feature.getId().substring( 1 ) );
141                    throw new TransactionException( msg );
142                }
143                LOG.logDebug( "Inserting root feature '" + feature.getId() + "'..." );
144                insertFeature( feature );
145                FeatureId fid = new FeatureId( ft, feature.getId() );
146                fids.add( fid );
147    
148            }
149    
150            // merge inserts rows that are identical (except their pks)
151            this.insertRows = mergeInsertRows( this.insertRows );
152    
153            // add featureRows to insertRows
154            Iterator<FeatureRow> iter = this.featuresInInsertion.values().iterator();
155            while ( iter.hasNext() ) {
156                this.insertRows.add( iter.next() );
157            }
158    
159            // check for cyclic fk constraints
160            Collection<InsertRow> cycle = InsertRow.findCycle( this.insertRows );
161            if ( cycle != null ) {
162                Iterator<InsertRow> cycleIter = cycle.iterator();
163                StringBuffer sb = new StringBuffer();
164                while ( cycleIter.hasNext() ) {
165                    sb.append( cycleIter.next() );
166                    if ( cycle.iterator().hasNext() ) {
167                        sb.append( " -> " );
168                    }
169                }
170                String msg = Messages.getMessage( "DATASTORE_FK_CYCLE", sb.toString() );
171                throw new TransactionException( msg );
172            }
173    
174            // sort the insert rows topologically
175            List<InsertRow> sortedInserts = InsertRow.sortInsertRows( this.insertRows );
176    
177            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
178                LOG.logDebug( sortedInserts.size() + " rows to be inserted: " );
179                for ( InsertRow row : sortedInserts ) {
180                    LOG.logDebug( row.toString() );
181                }
182            }
183    
184            executeInserts( sortedInserts );
185            return fids;
186        }
187    
188        /**
189         * Builds the <code>InsertRows</code> that are necessary to insert the given feature instance
190         * (including all properties + subfeatures).
191         * 
192         * @param feature
193         * @return InsertRows that are necessary to insert the given feature instance
194         * @throws TransactionException
195         */
196        private FeatureRow insertFeature( Feature feature )
197                                throws TransactionException {
198    
199            MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType();
200            if ( !ft.isInsertable() ) {
201                String msg = Messages.getMessage( "DATASTORE_FT_NOT_INSERTABLE", ft.getName() );
202                throw new TransactionException( msg );
203            }
204    
205            LOG.logDebug( "Creating InsertRow for feature with type '" + ft.getName() + "' and id: '" + feature.getId()
206                          + "'." );
207    
208            // extract feature id column value
209            MappingField[] fidFields = ft.getGMLId().getIdFields();
210            if ( fidFields.length > 1 ) {
211                throw new TransactionException( "Insertion of features with compound feature " + "ids is not supported." );
212            }
213            FeatureId fid = null;
214            try {
215                fid = new FeatureId( ft, feature.getId() );
216            } catch ( IdGenerationException e ) {
217                throw new TransactionException( e.getMessage(), e );
218            }
219    
220            // check if the feature id is already being inserted (happens for cyclic features)
221            FeatureRow insertRow = this.featuresInInsertion.get( fid );
222            if ( insertRow != null ) {
223                return insertRow;
224            }
225    
226            insertRow = new FeatureRow( ft.getTable() );
227            this.featuresInInsertion.put( fid, insertRow );
228    
229            // add column value for fid (primary key)
230            String fidColumn = fidFields[0].getField();
231            insertRow.setColumn( fidColumn, fid.getValue( 0 ), ft.getGMLId().getIdFields()[0].getType(), true );
232    
233            // process properties
234            FeatureProperty[] properties = feature.getProperties();
235            for ( int i = 0; i < properties.length; i++ ) {
236                FeatureProperty property = properties[i];
237                MappedPropertyType propertyType = (MappedPropertyType) ft.getProperty( property.getName() );
238                if ( propertyType == null ) {
239                    String msg = Messages.getMessage( "DATASTORE_PROPERTY_TYPE_NOT_KNOWN", property.getName() );
240                    LOG.logDebug( msg );
241                    throw new TransactionException( msg );
242                }
243                insertProperty( property, propertyType, insertRow );
244            }
245            return insertRow;
246        }
247    
248        /**
249         * Builds the <code>InsertRow</code>s that are necessary to insert the given property
250         * instance (including all it's subfeatures).
251         * 
252         * @param property
253         *            property instance to be inserted
254         * @param propertyType
255         *            property type of the property
256         * @param featureRow
257         *            table row of the parent feature instance
258         * @throws TransactionException
259         */
260        private void insertProperty( FeatureProperty property, MappedPropertyType propertyType, InsertRow featureRow )
261                                throws TransactionException {
262    
263            if ( propertyType instanceof SimplePropertyType ) {
264                LOG.logDebug( "- Simple property '" + propertyType.getName() + "', value='" + getPropertyValue( property )
265                              + "'." );
266                insertProperty( (MappedSimplePropertyType) propertyType, property, featureRow );
267            } else if ( propertyType instanceof GeometryPropertyType ) {
268                LOG.logDebug( "- Geometry property: '" + propertyType.getName() + "'" );
269                insertProperty( (MappedGeometryPropertyType) propertyType, property, featureRow );
270            } else if ( propertyType instanceof FeaturePropertyType ) {
271                LOG.logDebug( "- Feature property: '" + propertyType.getName() + "'" );
272                insertProperty( (MappedFeaturePropertyType) propertyType, property, featureRow );
273            } else {
274                throw new TransactionException( "Unhandled property type '" + propertyType.getClass().getName() + "'." );
275            }
276        }
277    
278        /**
279         * Inserts the given simple property (stored in feature table or in related table).
280         * 
281         * @param pt
282         * @param property
283         * @param featureRow
284         * @throws TransactionException
285         */
286        private void insertProperty( MappedSimplePropertyType pt, FeatureProperty property, InsertRow featureRow )
287                                throws TransactionException {
288    
289            SimpleContent content = pt.getContent();
290            if ( content.isUpdateable() ) {
291                if ( content instanceof MappingField ) {
292                    MappingField mf = (MappingField) content;
293                    String propertyColumn = mf.getField();
294                    Object propertyValue = property.getValue();
295                    int propertyType = mf.getType();
296                    TableRelation[] relations = pt.getTableRelations();
297                    insertProperty( propertyColumn, propertyValue, propertyType, relations, featureRow );
298                }
299            }
300        }
301    
302        /**
303         * Inserts the given geometry property (stored in feature table or in related table).
304         * 
305         * @param pt
306         * @param property
307         * @param featureRow
308         * @throws TransactionException
309         */
310        private void insertProperty( MappedGeometryPropertyType pt, FeatureProperty property, InsertRow featureRow )
311                                throws TransactionException {
312    
313            String propertyColumn = pt.getMappingField().getField();
314            MappingGeometryField dbField = pt.getMappingField();
315            Geometry deegreeGeometry = (Geometry) property.getValue();
316            Object dbGeometry;
317    
318            int createSrsCode = dbField.getSRS();
319            int targetSrsCode = -1;
320            if ( deegreeGeometry.getCoordinateSystem() == null ) {
321                LOG.logDebug( "No SRS information for geometry available. Assuming '" + pt.getSRS() + "'." );
322            } else if ( !pt.getSRS().toString().equals( deegreeGeometry.getCoordinateSystem().getName() ) ) {
323                String msg = "Insert-Transformation: geometry srs: " + deegreeGeometry.getCoordinateSystem().getName()
324                             + " -> property srs: " + pt.getSRS();
325                LOG.logDebug( msg );
326                if ( createSrsCode == -1 ) {
327                    msg = Messages.getMessage( "DATASTORE_SRS_NOT_SPECIFIED", pt.getName(),
328                                               deegreeGeometry.getCoordinateSystem(), pt.getSRS() );
329                    throw new TransactionException( msg );
330                }
331                try {
332                    createSrsCode = datastore.getNativeSRSCode( deegreeGeometry.getCoordinateSystem().getName() );
333                } catch ( DatastoreException e ) {
334                    throw new TransactionException( e.getMessage(), e );
335                }
336                targetSrsCode = dbField.getSRS();
337            }
338    
339            try {
340                dbGeometry = this.datastore.convertDeegreeToDBGeometry( deegreeGeometry, createSrsCode, this.conn );
341            } catch ( DatastoreException e ) {
342                throw new TransactionException( e.getMessage(), e );
343            }
344    
345            int propertyType = pt.getMappingField().getType();
346    
347            // TODO remove this Oracle hack
348            if ( this.datastore.getClass().getName().contains( "OracleDatastore" ) ) {
349                propertyType = Types.STRUCT;
350            }
351    
352            TableRelation[] relations = pt.getTableRelations();
353            insertProperty( propertyColumn, dbGeometry, propertyType, relations, featureRow, targetSrsCode );
354        }
355    
356        /**
357         * Inserts the given simple property (stored in feature table or in related table).
358         * 
359         * @param propertyColumn
360         * @param propertyValue
361         * @param propertyType
362         * @param featureRow
363         * @throws TransactionException
364         */
365        private void insertProperty( String propertyColumn, Object propertyValue, int propertyType,
366                                     TableRelation[] relations, InsertRow featureRow )
367                                throws TransactionException {
368    
369            if ( relations == null || relations.length == 0 ) {
370                // property is stored in feature table
371                featureRow.setColumn( propertyColumn, propertyValue, propertyType, false );
372            } else {
373                // property is stored in related table
374                if ( relations.length > 1 ) {
375                    throw new TransactionException( Messages.getMessage( "DATASTORE_SIMPLE_PROPERTY_JOIN" ) );
376                }
377    
378                if ( !relations[0].isFromFK() ) {
379                    // fk is in property table
380                    MappingField[] pkFields = relations[0].getFromFields();
381                    MappingField[] fkFields = relations[0].getToFields();
382    
383                    for ( int i = 0; i < pkFields.length; i++ ) {
384                        InsertField pkField = featureRow.getColumn( pkFields[i].getField() );
385                        if ( pkField == null ) {
386                            String msg = Messages.getMessage( "DATASTORE_NO_FK_VALUE", pkField.getColumnName(),
387                                                              pkFields[i].getTable() );
388                            throw new TransactionException( msg );
389                        }
390                        int pkColumnType = pkField.getSQLType();
391                        int fkColumnType = fkFields[i].getType();
392                        if ( pkColumnType != fkColumnType ) {
393                            String fkType = "" + fkColumnType;
394                            String pkType = "" + pkColumnType;
395                            try {
396                                fkType = Types.getTypeNameForSQLTypeCode( fkColumnType );
397                                pkType = Types.getTypeNameForSQLTypeCode( pkColumnType );
398                            } catch ( UnknownTypeException e ) {
399                                LOG.logError( e.getMessage(), e );
400                            }
401                            Object[] params = new Object[] { relations[0].getToTable(), fkFields[i].getField(), fkType,
402                                                            featureRow.getTable(), pkFields[i].getField(), pkType };
403                            String msg = Messages.getMessage( "DATASTORE_FK_PK_TYPE_MISMATCH", params );
404                            throw new TransactionException( msg );
405                        }
406                        InsertRow insertRow = new InsertRow( relations[0].getToTable() );
407                        insertRow.linkColumn( fkFields[i].getField(), pkField );
408                        insertRow.setColumn( propertyColumn, propertyValue, propertyType, false );
409                        this.insertRows.add( insertRow );
410                    }
411                } else {
412                    // fk is in feature table
413                    MappingField[] pkFields = relations[0].getToFields();
414                    MappingField[] fkFields = relations[0].getFromFields();
415    
416                    // generate necessary primary key value
417                    InsertField pkField = null;
418                    try {
419                        Object pk = null;
420                        // TODO remove hack!!!
421                        if ( relations[0].getIdGenerator() instanceof ParentIDGenerator ) {
422                            InsertField field = featureRow.getColumn( "ID" );
423                            if ( field == null ) {
424                                throw new TransactionException( "No value for ID available!" );
425                            }
426                            pk = field.getValue();
427                        } else {
428                            pk = relations[0].getNewPK( this.dsTa );
429                        }
430                        InsertRow insertRow = findOrCreateRow( relations[0].getToTable(), pkFields[0].getField(), pk );
431                        pkField = insertRow.setColumn( pkFields[0].getField(), pk, pkFields[0].getType(), true );
432                        insertRow.setColumn( propertyColumn, propertyValue, propertyType, false );
433                    } catch ( IdGenerationException e ) {
434                        throw new TransactionException( e.getMessage(), e );
435                    }
436                    featureRow.linkColumn( fkFields[0].getField(), pkField );
437                }
438            }
439        }
440    
441        /**
442         * Inserts the given geometry property (stored in feature table or in related table).
443         * 
444         * @param propertyColumn
445         * @param propertyValue
446         * @param propertyType
447         * @param featureRow
448         * @throws TransactionException
449         */
450        private void insertProperty( String propertyColumn, Object propertyValue, int propertyType,
451                                     TableRelation[] relations, InsertRow featureRow, int targetSrsCode )
452                                throws TransactionException {
453    
454            if ( relations == null || relations.length == 0 ) {
455                // property is stored in feature table
456                featureRow.setGeometryColumn( propertyColumn, propertyValue, propertyType, false, targetSrsCode );
457            } else {
458                // property is stored in related table
459                if ( relations.length > 1 ) {
460                    throw new TransactionException( Messages.getMessage( "DATASTORE_SIMPLE_PROPERTY_JOIN" ) );
461                }
462    
463                if ( !relations[0].isFromFK() ) {
464                    // fk is in property table
465                    MappingField[] pkFields = relations[0].getFromFields();
466                    MappingField[] fkFields = relations[0].getToFields();
467    
468                    for ( int i = 0; i < pkFields.length; i++ ) {
469                        InsertField pkField = featureRow.getColumn( pkFields[i].getField() );
470                        if ( pkField == null ) {
471                            String msg = Messages.getMessage( "DATASTORE_NO_FK_VALUE", pkField.getColumnName(),
472                                                              pkFields[i].getTable() );
473                            throw new TransactionException( msg );
474                        }
475                        int pkColumnType = pkField.getSQLType();
476                        int fkColumnType = fkFields[i].getType();
477                        if ( pkColumnType != fkColumnType ) {
478                            String fkType = "" + fkColumnType;
479                            String pkType = "" + pkColumnType;
480                            try {
481                                fkType = Types.getTypeNameForSQLTypeCode( fkColumnType );
482                                pkType = Types.getTypeNameForSQLTypeCode( pkColumnType );
483                            } catch ( UnknownTypeException e ) {
484                                LOG.logError( e.getMessage(), e );
485                            }
486                            Object[] params = new Object[] { relations[0].getToTable(), fkFields[i].getField(), fkType,
487                                                            featureRow.getTable(), pkFields[i].getField(), pkType };
488                            String msg = Messages.getMessage( "DATASTORE_FK_PK_TYPE_MISMATCH", params );
489                            throw new TransactionException( msg );
490                        }
491                        InsertRow insertRow = new InsertRow( relations[0].getToTable() );
492                        insertRow.linkColumn( fkFields[i].getField(), pkField );
493                        insertRow.setGeometryColumn( propertyColumn, propertyValue, propertyType, false, targetSrsCode );
494                        this.insertRows.add( insertRow );
495                    }
496                } else {
497                    // fk is in feature table
498                    MappingField[] pkFields = relations[0].getToFields();
499                    MappingField[] fkFields = relations[0].getFromFields();
500    
501                    // generate necessary primary key value
502                    InsertField pkField = null;
503                    try {
504                        Object pk = null;
505                        // TODO remove hack!!!
506                        if ( relations[0].getIdGenerator() instanceof ParentIDGenerator ) {
507                            InsertField field = featureRow.getColumn( "ID" );
508                            if ( field == null ) {
509                                throw new TransactionException( "No value for ID available!" );
510                            }
511                            pk = field.getValue();
512                        } else {
513                            pk = relations[0].getNewPK( this.dsTa );
514                        }
515                        InsertRow insertRow = findOrCreateRow( relations[0].getToTable(), pkFields[0].getField(), pk );
516                        pkField = insertRow.setColumn( pkFields[0].getField(), pk, pkFields[0].getType(), true );
517                        insertRow.setGeometryColumn( propertyColumn, propertyValue, propertyType, false, targetSrsCode );
518                    } catch ( IdGenerationException e ) {
519                        throw new TransactionException( e.getMessage(), e );
520                    }
521                    featureRow.linkColumn( fkFields[0].getField(), pkField );
522                }
523            }
524        }
525    
526        /**
527         * Inserts the given feature property.
528         * 
529         * @param pt
530         * @param property
531         * @param featureRow
532         * @throws TransactionException
533         */
534        private void insertProperty( MappedFeaturePropertyType pt, FeatureProperty property, InsertRow featureRow )
535                                throws TransactionException {
536    
537            // find (concrete) subfeature type for the given property instance
538            MappedFeatureType propertyFeatureType = pt.getFeatureTypeReference().getFeatureType();
539            MappedFeatureType[] substitutions = propertyFeatureType.getConcreteSubstitutions();
540            Feature subFeature = (Feature) property.getValue();
541            MappedFeatureType subFeatureType = null;
542            for ( int i = 0; i < substitutions.length; i++ ) {
543                if ( substitutions[i].getName().equals( subFeature.getName() ) ) {
544                    subFeatureType = substitutions[i];
545                    break;
546                }
547            }
548            if ( subFeatureType == null ) {
549                String msg = Messages.getMessage( "DATASTORE_FEATURE_NOT_SUBSTITUTABLE", propertyFeatureType.getName(),
550                                                  subFeature.getName() );
551                throw new TransactionException( msg );
552            }
553            boolean needsDisambiguation = propertyFeatureType.hasSeveralImplementations();
554    
555            TableRelation[] relations = pt.getTableRelations();
556            if ( relations == null || relations.length < 1 ) {
557                throw new TransactionException( "Invalid feature property definition, feature property "
558                                                + "mappings must use at least one 'TableRelation' element." );
559            }
560    
561            // workaround for links to dummy InsertRows (of already stored features)
562            boolean cutLink = subFeature.getId().startsWith( FeatureIdAssigner.EXISTS_MARKER );
563            InsertRow subFeatureRow = null;
564            if ( cutLink ) {
565                try {
566                    Object fidValue = FeatureId.removeFIDPrefix( subFeature.getId().substring( 1 ),
567                                                                 subFeatureType.getGMLId() );
568                    subFeatureRow = new FeatureRow( subFeatureType.getTable() );
569                    // add column value for fid (primary key)
570                    String fidColumn = subFeatureType.getGMLId().getIdFields()[0].getField();
571                    subFeatureRow.setColumn( fidColumn, fidValue, subFeatureType.getGMLId().getIdFields()[0].getType(),
572                                             true );
573                } catch ( DatastoreException e ) {
574                    throw new TransactionException( e );
575                }
576            } else {
577                // insert sub feature (if it is not already stored)
578                subFeatureRow = insertFeature( subFeature );
579            }
580    
581            if ( relations.length == 1 ) {
582                if ( relations[0].isFromFK() ) {
583                    // fk is in feature table
584                    MappingField[] pkFields = relations[0].getToFields();
585                    MappingField[] fkFields = relations[0].getFromFields();
586    
587                    for ( int i = 0; i < pkFields.length; i++ ) {
588                        InsertField pkField = subFeatureRow.getColumn( pkFields[i].getField() );
589                        if ( pkField == null ) {
590                            String msg = Messages.getMessage( "DATASTORE_NO_FK_VALUE", pkField.getColumnName(),
591                                                              pkField.getTable() );
592                            throw new TransactionException( msg );
593                        }
594                        int pkColumnType = pkField.getSQLType();
595                        int fkColumnType = fkFields[i].getType();
596                        if ( pkColumnType != fkColumnType ) {
597                            String fkType = "" + fkColumnType;
598                            String pkType = "" + pkColumnType;
599                            try {
600                                fkType = Types.getTypeNameForSQLTypeCode( fkColumnType );
601                                pkType = Types.getTypeNameForSQLTypeCode( pkColumnType );
602                            } catch ( UnknownTypeException e ) {
603                                LOG.logError( e.getMessage(), e );
604                            }
605                            Object[] params = new Object[] { featureRow.getTable(), fkFields[i].getField(), fkType,
606                                                            subFeatureRow.getTable(), pkFields[i].getField(), pkType };
607                            String msg = Messages.getMessage( "DATASTORE_FK_PK_TYPE_MISMATCH", params );
608                            throw new TransactionException( msg );
609                        }
610    
611                        if ( !cutLink ) {
612                            featureRow.linkColumn( fkFields[i].getField(), pkField );
613                        } else {
614                            featureRow.setColumn( fkFields[i].getField(), pkField.getValue(), pkField.getSQLType(), false );
615                        }
616                    }
617    
618                    if ( needsDisambiguation ) {
619                        String typeField = FT_PREFIX + relations[0].getFromFields()[0].getField();
620                        featureRow.setColumn( typeField, subFeatureType.getName().getLocalName(), Types.VARCHAR, false );
621                    }
622                } else {
623                    // fk is in subfeature table
624                    MappingField[] pkFields = relations[0].getFromFields();
625                    MappingField[] fkFields = relations[0].getToFields();
626    
627                    if ( pkFields[0] != null ) {
628                        LOG.logDebug( "Getting column " + pkFields[0].getField() + "from table: " + pkFields[0].getTable()
629                                      + " of the featureRow: " + featureRow.getTable() );
630                    }
631    
632                    InsertField pkField = featureRow.getColumn( pkFields[0].getField() );
633    
634                    if ( pkField == null ) {
635                        String msg = null;
636    
637                        if ( pkFields[0] != null ) {
638                            msg = Messages.getMessage( "DATASTORE_NO_FK_VALUE", pkFields[0].getField(),
639                                                       pkFields[0].getTable() );
640                        } else {
641                            if ( relations[0] != null ) {
642                                msg = Messages.getMessage( "DATASTORE_NO_FK_VALUE",
643                                                           "unknown primary keys in 'from'-fields",
644                                                           relations[0].getFromTable() );
645                            } else {
646                                msg = Messages.getMessage( "DATASTORE_NO_FK_VALUE",
647                                                           "unknown primary keys in 'from'-fields", "unknown 'from'-table" );
648                            }
649                        }
650    
651                        throw new TransactionException( msg );
652                    }
653                    int pkColumnType = pkField.getSQLType();
654                    int fkColumnType = fkFields[0].getType();
655                    if ( pkColumnType != fkColumnType ) {
656                        String fkType = "" + fkColumnType;
657                        String pkType = "" + pkColumnType;
658                        try {
659                            fkType = Types.getTypeNameForSQLTypeCode( fkColumnType );
660                            pkType = Types.getTypeNameForSQLTypeCode( pkColumnType );
661                        } catch ( UnknownTypeException e ) {
662                            LOG.logError( e.getMessage(), e );
663                        }
664                        Object[] params = new Object[] { subFeatureRow.getTable(), fkFields[0].getField(), fkType,
665                                                        featureRow.getTable(), pkField.getColumnName(), pkType };
666                        String msg = Messages.getMessage( "DATASTORE_FK_PK_TYPE_MISMATCH", params );
667                        throw new TransactionException( msg );
668                    }
669    
670                    if ( !cutLink ) {
671                        subFeatureRow.linkColumn( fkFields[0].getField(), pkField );
672                    } else {
673                        subFeatureRow.setColumn( fkFields[0].getField(), pkField.getValue(), pkField.getSQLType(), false );
674                    }
675                }
676            } else if ( relations.length == 2 ) {
677    
678                // insert into join table
679                String joinTable = relations[0].getToTable();
680                MappingField[] leftKeyFields = relations[0].getToFields();
681                MappingField[] rightKeyFields = relations[1].getFromFields();
682    
683                InsertRow jtRow = new InsertRow( joinTable );
684                if ( needsDisambiguation ) {
685                    jtRow.setColumn( FT_COLUMN, subFeatureType.getName().getLocalName(), Types.VARCHAR, false );
686                }
687    
688                if ( !relations[0].isFromFK() ) {
689                    // left key field in join table is fk
690                    MappingField[] pkFields = relations[0].getFromFields();
691                    InsertField pkField = featureRow.getColumn( pkFields[0].getField() );
692                    if ( pkField == null ) {
693                        String columnName = null;
694                        if ( pkFields[0] != null ) {
695                            columnName = pkFields[0].getField();
696                        } else {
697                            columnName = "unknown primary keys in 'from'-fields";
698                        }
699                        throw new TransactionException( "Insertion of feature property using join table failed: "
700                                                        + "no value for join table key column '" + columnName + "'." );
701                    }
702                    jtRow.linkColumn( leftKeyFields[0].getField(), pkField );
703                } else {
704                    // left key field in join table is pk
705                    MappingField[] pkFields = relations[0].getToFields();
706                    // generate necessary primary key value
707                    InsertField pkField = null;
708                    try {
709                        Object pk = relations[0].getNewPK( this.dsTa );
710                        pkField = jtRow.setColumn( pkFields[0].getField(), pk, pkFields[0].getType(), true );
711                    } catch ( IdGenerationException e ) {
712                        throw new TransactionException( e.getMessage(), e );
713                    }
714                    featureRow.linkColumn( relations[0].getFromFields()[0].getField(), pkField );
715                }
716    
717                if ( relations[1].isFromFK() ) {
718                    // right key field in join table is fk
719                    MappingField[] pkFields = relations[1].getToFields();
720                    InsertField pkField = subFeatureRow.getColumn( pkFields[0].getField() );
721                    if ( pkField == null ) {
722                        throw new TransactionException( "Insertion of feature property using join table failed: "
723                                                        + "no value for join table key column '" + pkField.getColumnName()
724                                                        + "'." );
725                    }
726                    if ( !cutLink ) {
727                        jtRow.linkColumn( rightKeyFields[0].getField(), pkField );
728                    } else {
729                        jtRow.setColumn( rightKeyFields[0].getField(), pkField.getValue(), pkField.getSQLType(), false );
730                    }
731                } else {
732                    // right key field in join table is pk
733                    MappingField[] pkFields = relations[1].getFromFields();
734                    // generate necessary primary key value
735                    InsertField pkField = null;
736                    try {
737                        Object pk = relations[1].getNewPK( this.dsTa );
738                        pkField = jtRow.setColumn( pkFields[0].getField(), pk, pkFields[0].getType(), true );
739                    } catch ( IdGenerationException e ) {
740                        throw new TransactionException( e.getMessage(), e );
741                    }
742                    if ( !cutLink ) {
743                        subFeatureRow.linkColumn( relations[1].getToFields()[0].getField(), pkField );
744                    }
745                }
746                this.insertRows.add( jtRow );
747            } else {
748                throw new TransactionException( "Insertion of feature properties stored in related tables "
749                                                + "connected via more than one join table is not supported." );
750            }
751        }
752    
753        /**
754         * Checks whether the feature that corresponds to the given FeatureRow is already stored in the
755         * database.
756         * 
757         * @param featureRow
758         * @return true, if feature is already stored, false otherwise
759         * @throws DatastoreException
760         */
761        private boolean doesFeatureExist( FeatureRow featureRow )
762                                throws DatastoreException {
763    
764            boolean exists = false;
765    
766            InsertField pkField = featureRow.getPKColumn();
767    
768            StatementBuffer query = buildFeatureSelect( pkField.getColumnName(), pkField.getSQLType(), pkField.getValue(),
769                                                        featureRow.getTable() );
770            LOG.logDebug( "Feature existence query: '" + query + "'" );
771    
772            PreparedStatement stmt = null;
773            ResultSet rs = null;
774            try {
775                stmt = this.datastore.prepareStatement( this.conn, query );
776                rs = stmt.executeQuery();
777                if ( rs.next() ) {
778                    exists = true;
779                }
780                if ( rs.next() ) {
781                    String msg = Messages.getMessage( "DATASTORE_FEATURE_QUERY_MORE_THAN_ONE_RESULT",
782                                                      query.getQueryString() );
783                    LOG.logError( msg );
784                    throw new TransactionException( msg );
785                }
786            } catch ( SQLException e ) {
787                throw new TransactionException( e );
788            } finally {
789                try {
790                    if ( rs != null ) {
791                        rs.close();
792                    }
793                } catch ( SQLException e ) {
794                    throw new TransactionException( e );
795                } finally {
796                    if ( stmt != null ) {
797                        try {
798                            stmt.close();
799                        } catch ( SQLException e ) {
800                            throw new TransactionException( e );
801                        }
802                    }
803                }
804            }
805            return exists;
806        }
807    
808        /**
809         * Builds a SELECT statement that checks for the existence of a feature with the given id.
810         * 
811         * @param fidColumn
812         * @param typeCode
813         * @param fidValue
814         * @param table
815         * @return the statement
816         */
817        private StatementBuffer buildFeatureSelect( String fidColumn, int typeCode, Object fidValue, String table ) {
818    
819            StatementBuffer query = new StatementBuffer();
820            query.append( "SELECT * FROM " );
821            query.append( table );
822            query.append( " WHERE " );
823    
824            // append feature id constraints
825            query.append( fidColumn );
826            query.append( "=?" );
827            query.addArgument( fidValue, typeCode );
828            return query;
829        }
830    
831        private InsertRow findOrCreateRow( String table, String pkColumn, Object value ) {
832            Iterator<InsertRow> rowIter = this.insertRows.iterator();
833            boolean found = false;
834            InsertRow row = null;
835            while ( rowIter.hasNext() ) {
836                row = rowIter.next();
837                if ( row.getTable().equals( table ) ) {
838                    InsertField field = row.getColumn( pkColumn );
839                    if ( value.equals( field.getValue() ) ) {
840                        found = true;
841                        LOG.logDebug( "Found matching row " + row );
842                        break;
843                    }
844                }
845            }
846            if ( !found ) {
847                row = new InsertRow( table );
848                this.insertRows.add( row );
849            }
850            return row;
851        }
852    
853        private String getPropertyValue( FeatureProperty property ) {
854            Object value = property.getValue();
855            StringBuffer sb = new StringBuffer();
856            if ( value instanceof Object[] ) {
857                Object[] objects = (Object[]) value;
858                for ( int i = 0; i < objects.length; i++ ) {
859                    sb.append( objects[i] );
860                }
861            } else {
862                sb.append( value );
863            }
864            return sb.toString();
865        }
866    
867        /**
868         * Transforms the given <code>List</code> of <code>InsertRows</code> into SQL INSERT
869         * statements and executes them using the underlying JDBC connection.
870         * 
871         * @param inserts
872         * @throws DatastoreException
873         */
874        private void executeInserts( List<InsertRow> inserts )
875                                throws DatastoreException {
876    
877            PreparedStatement stmt = null;
878    
879            for ( InsertRow row : inserts ) {
880                if ( row instanceof FeatureRow ) {
881                    if ( doesFeatureExist( (FeatureRow) row ) ) {
882                        LOG.logDebug( "Skipping feature row. Already present in db." );
883                        continue;
884                    }
885                }
886                try {
887                    stmt = null;
888                    StatementBuffer insert = createStatementBuffer( row );
889                    LOG.logDebug( insert.toString() );
890                    stmt = this.datastore.prepareStatement( this.conn, insert );
891                    stmt.execute();
892                } catch ( SQLException e ) {
893                    String msg = "Error performing insert: " + e.getMessage();
894                    LOG.logError( msg, e );
895                    throw new TransactionException( msg, e );
896                } finally {
897                    if ( stmt != null ) {
898                        try {
899                            stmt.close();
900                        } catch ( SQLException e ) {
901                            String msg = "Error closing statement: " + e.getMessage();
902                            LOG.logError( msg, e );
903                        }
904                    }
905                }
906            }
907        }
908    
909        private StatementBuffer createStatementBuffer( InsertRow row )
910                                throws DatastoreException {
911            StatementBuffer insert = new StatementBuffer();
912            insert.append( "INSERT INTO " );
913            insert.append( row.table );
914            insert.append( " (" );
915            Iterator<InsertField> columnsIter = row.getColumns().iterator();
916            while ( columnsIter.hasNext() ) {
917                insert.append( columnsIter.next().getColumnName() );
918                if ( columnsIter.hasNext() ) {
919                    insert.append( ',' );
920                }
921            }
922            insert.append( ") VALUES(" );
923            columnsIter = row.getColumns().iterator();
924            while ( columnsIter.hasNext() ) {
925                String placeHolder = "?";
926                InsertField field = columnsIter.next();
927                if ( field instanceof InsertGeometryField ) {
928                    int targetSrsCode = ( (InsertGeometryField) field ).getTargetSrsCode();
929                    if ( targetSrsCode != -1 ) {
930                        placeHolder = this.datastore.buildSRSTransformCall( "?", targetSrsCode );
931                    }
932                }
933                insert.append( placeHolder );
934                insert.addArgument( field.getValue(), field.getSQLType() );
935                if ( columnsIter.hasNext() ) {
936                    insert.append( ',' );
937                }
938            }
939            insert.append( ")" );
940            return insert;
941        }
942    
943        /**
944         * Merges the given <code>InsertRow</code>s by eliminating rows that have identical content
945         * (except for their primary keys).
946         * <p>
947         * This only applies to non-FeatureRows: there are never two FeatureRows that may be treated as
948         * identical, because unique feature ids have been assigned to them before.
949         * 
950         * @see FeatureIdAssigner
951         * 
952         * @param insertRows
953         * @return merged List of InsertRows
954         */
955        private List<InsertRow> mergeInsertRows( List<InsertRow> insertRows ) {
956    
957            List<InsertRow> result = new ArrayList<InsertRow>();
958    
959            // keys: table names, values: inserts into the table
960            Map<String, Collection<InsertRow>> tableMap = new HashMap<String, Collection<InsertRow>>();
961    
962            // build table lookup map
963            Iterator<InsertRow> iter = insertRows.iterator();
964            while ( iter.hasNext() ) {
965                InsertRow insertRow = iter.next();
966                Collection<InsertRow> tableInserts = tableMap.get( insertRow.getTable() );
967                if ( tableInserts == null ) {
968                    tableInserts = new ArrayList<InsertRow>();
969                    tableMap.put( insertRow.getTable(), tableInserts );
970                }
971                tableInserts.add( insertRow );
972            }
973    
974            iter = insertRows.iterator();
975            while ( iter.hasNext() ) {
976                InsertRow insertRow = iter.next();
977                boolean insert = true;
978                if ( !( insertRow instanceof FeatureRow ) ) {
979                    Collection<InsertRow> tableInserts = tableMap.get( insertRow.getTable() );
980                    Iterator<InsertRow> candidatesIter = tableInserts.iterator();
981                    while ( candidatesIter.hasNext() ) {
982                        InsertRow candidate = candidatesIter.next();
983                        if ( insertRow != candidate ) {
984                            if ( compareInsertRows( insertRow, candidate ) ) {
985                                LOG.logDebug( "Removing InsertRow: " + insertRow.hashCode() + " " + insertRow
986                                              + " - duplicate of: " + candidate );
987                                replaceInsertRow( insertRow, candidate );
988                                insert = false;
989                                tableInserts.remove( insertRow );
990                                break;
991                            }
992                        }
993                    }
994                }
995                if ( insert ) {
996                    result.add( insertRow );
997                }
998            }
999            return result;
1000        }
1001    
1002        private boolean compareInsertRows( InsertRow row1, InsertRow row2 ) {
1003            Collection<InsertField> fields1 = row1.getColumns();
1004            Iterator<InsertField> iter = fields1.iterator();
1005            while ( iter.hasNext() ) {
1006                InsertField field1 = iter.next();
1007                if ( !field1.isPK() ) {
1008                    InsertField field2 = row2.getColumn( field1.getColumnName() );
1009                    Object value1 = field1.getValue();
1010                    Object value2 = null;
1011                    if ( field2 != null )
1012                        value2 = field2.getValue();
1013                    if ( value1 == null ) {
1014                        if ( value2 == null ) {
1015                            continue;
1016                        }
1017                        return false;
1018                    }
1019                    try {
1020                        if ( !value1.equals( value2 ) ) {
1021                            return false;
1022                        }
1023                    } catch ( NullPointerException e ) {
1024                        LOG.logWarning( "A null pointer exception occurred while comparing features/attributes. Assuming they're not equal..." );
1025                        return false;
1026                    }
1027                }
1028            }
1029            return true;
1030        }
1031    
1032        private void replaceInsertRow( InsertRow oldRow, InsertRow newRow ) {
1033    
1034            Collection<InsertField> oldFields = oldRow.getColumns();
1035            for ( InsertField field : oldFields ) {
1036                InsertField toField = field.getReferencedField();
1037                if ( toField != null ) {
1038                    LOG.logDebug( "Removing reference to field '" + toField + "'" );
1039                    toField.removeReferencingField( field );
1040                }
1041            }
1042    
1043            Collection<InsertField> referencingFields = oldRow.getReferencingFields();
1044            for ( InsertField fromField : referencingFields ) {
1045                LOG.logDebug( "Replacing reference for field '" + fromField + "'" );
1046                InsertField field = newRow.getColumn( fromField.getReferencedField().getColumnName() );
1047                LOG.logDebug( "" + field );
1048                fromField.relinkField( field );
1049            }
1050        }
1051    }