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