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