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