001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/tags/2.1/src/org/deegree/io/datastore/sql/FeatureFetcher.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2007 by:
006     Department of Geography, University of Bonn
007     http://www.giub.uni-bonn.de/deegree/
008     lat/lon GmbH
009     http://www.lat-lon.de
010    
011     This library is free software; you can redistribute it and/or
012     modify it under the terms of the GNU Lesser General Public
013     License as published by the Free Software Foundation; either
014     version 2.1 of the License, or (at your option) any later version.
015    
016     This library is distributed in the hope that it will be useful,
017     but WITHOUT ANY WARRANTY; without even the implied warranty of
018     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
019     Lesser General Public License for more details.
020    
021     You should have received a copy of the GNU Lesser General Public
022     License along with this library; if not, write to the Free Software
023     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
024    
025     Contact:
026    
027     Andreas Poth
028     lat/lon GmbH
029     Aennchenstraße 19
030     53177 Bonn
031     Germany
032     E-Mail: poth@lat-lon.de
033    
034     Prof. Dr. Klaus Greve
035     Department of Geography
036     University of Bonn
037     Meckenheimer Allee 166
038     53115 Bonn
039     Germany
040     E-Mail: greve@giub.uni-bonn.de
041     
042     ---------------------------------------------------------------------------*/
043    package org.deegree.io.datastore.sql;
044    
045    import java.sql.Connection;
046    import java.sql.PreparedStatement;
047    import java.sql.ResultSet;
048    import java.sql.SQLException;
049    import java.util.ArrayList;
050    import java.util.Collection;
051    import java.util.HashMap;
052    import java.util.HashSet;
053    import java.util.List;
054    import java.util.Map;
055    import java.util.Set;
056    
057    import org.deegree.datatypes.QualifiedName;
058    import org.deegree.framework.log.ILogger;
059    import org.deegree.framework.log.LoggerFactory;
060    import org.deegree.framework.util.StringTools;
061    import org.deegree.i18n.Messages;
062    import org.deegree.io.datastore.DatastoreException;
063    import org.deegree.io.datastore.FeatureId;
064    import org.deegree.io.datastore.PropertyPathResolver;
065    import org.deegree.io.datastore.schema.MappedFeaturePropertyType;
066    import org.deegree.io.datastore.schema.MappedFeatureType;
067    import org.deegree.io.datastore.schema.MappedGMLId;
068    import org.deegree.io.datastore.schema.MappedGeometryPropertyType;
069    import org.deegree.io.datastore.schema.MappedPropertyType;
070    import org.deegree.io.datastore.schema.MappedSimplePropertyType;
071    import org.deegree.io.datastore.schema.TableRelation;
072    import org.deegree.io.datastore.schema.content.ConstantContent;
073    import org.deegree.io.datastore.schema.content.MappingField;
074    import org.deegree.io.datastore.schema.content.MappingGeometryField;
075    import org.deegree.io.datastore.schema.content.SQLFunctionCall;
076    import org.deegree.io.datastore.schema.content.SimpleContent;
077    import org.deegree.model.crs.CRSFactory;
078    import org.deegree.model.crs.CoordinateSystem;
079    import org.deegree.model.crs.UnknownCRSException;
080    import org.deegree.model.feature.Feature;
081    import org.deegree.model.feature.FeatureFactory;
082    import org.deegree.model.feature.FeatureProperty;
083    import org.deegree.model.feature.schema.PropertyType;
084    import org.deegree.model.spatialschema.Geometry;
085    import org.deegree.ogcbase.PropertyPath;
086    import org.deegree.ogcwebservices.wfs.operation.Query;
087    
088    /**
089     * The only implementation of this abstract class is the {@link QueryHandler} class.
090     * <p>
091     * While the {@link QueryHandler} class performs the initial SELECT, {@link FeatureFetcher} is responsible for any
092     * subsequent SELECTs that may be necessary.
093     * 
094     * @see QueryHandler
095     * 
096     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
097     * @author last edited by: $Author: mschneider $
098     * 
099     * @version $Revision: 7782 $, $Date: 2007-07-17 18:40:42 +0200 (Di, 17 Jul 2007) $
100     */
101    abstract class FeatureFetcher extends AbstractRequestHandler {
102    
103        private static final ILogger LOG = LoggerFactory.getLogger( FeatureFetcher.class );
104    
105        // key: feature id of features that are generated or are in generation
106        protected Set<FeatureId> featuresInGeneration = new HashSet<FeatureId>();
107    
108        // key: feature id value, value: Feature
109        protected Map<FeatureId, Feature> featureMap = new HashMap<FeatureId, Feature>( 1000 );
110    
111        // key: feature id value, value: property instances that contain the feature
112        protected Map<FeatureId, List<FeatureProperty>> fidToPropertyMap = new HashMap<FeatureId, List<FeatureProperty>>();
113    
114        // provides virtual content (constants, sql functions, ...)
115        protected VirtualContentProvider vcProvider;
116    
117        protected Query query;
118    
119        private CoordinateSystem queryCS;
120    
121        // key: geometry field, value: function call that transforms it to the queried CS
122        private Map<MappingGeometryField, SQLFunctionCall> fieldToTransformCall = new HashMap<MappingGeometryField, SQLFunctionCall>();
123    
124        FeatureFetcher( AbstractSQLDatastore datastore, TableAliasGenerator aliasGenerator, Connection conn, Query query )
125                                throws DatastoreException {
126            super( datastore, aliasGenerator, conn );
127            this.query = query;
128            if ( this.query.getSrsName() != null ) {
129                try {
130                    this.queryCS = CRSFactory.create( this.query.getSrsName() );
131                } catch ( UnknownCRSException e ) {
132                    throw new DatastoreException( e.getMessage(), e );
133                }
134            }
135        }
136    
137        /**
138         * Builds a SELECT statement to fetch features / properties that are stored in a related table.
139         * 
140         * @param fetchContents
141         *            table columns / functions to fetch
142         * @param relations
143         *            table relations that lead to the table where the property is stored
144         * @param resultValues
145         *            all retrieved columns from one result set row
146         * @param resultPosMap
147         *            key class: SimpleContent, value class: Integer (this is the associated index in resultValues)
148         * @return the statement or null if the keys in resultValues contain NULL values
149         */
150        private StatementBuffer buildSubsequentSelect( List<List<SimpleContent>> fetchContents, TableRelation[] relations,
151                                                       Object[] resultValues, Map<SimpleContent, Integer> resultPosMap ) {
152    
153            this.aliasGenerator.reset();
154            String[] tableAliases = this.aliasGenerator.generateUniqueAliases( relations.length );
155    
156            StatementBuffer query = new StatementBuffer();
157            query.append( "SELECT " );
158            appendQualifiedContentList( query, tableAliases[tableAliases.length - 1], fetchContents );
159            query.append( " FROM " );
160            query.append( relations[0].getToTable() );
161            query.append( " " );
162            query.append( tableAliases[0] );
163    
164            // append joins
165            for ( int i = 1; i < relations.length; i++ ) {
166                query.append( " JOIN " );
167                query.append( relations[i].getToTable() );
168                query.append( " " );
169                query.append( tableAliases[i] );
170                query.append( " ON " );
171                MappingField[] fromFields = relations[i].getFromFields();
172                MappingField[] toFields = relations[i].getToFields();
173                for ( int j = 0; j < fromFields.length; j++ ) {
174                    query.append( tableAliases[i - 1] );
175                    query.append( '.' );
176                    query.append( fromFields[j].getField() );
177                    query.append( '=' );
178                    query.append( tableAliases[i] );
179                    query.append( '.' );
180                    query.append( toFields[j].getField() );
181                }
182            }
183    
184            // append key constraints
185            query.append( " WHERE " );
186            MappingField[] fromFields = relations[0].getFromFields();
187            MappingField[] toFields = relations[0].getToFields();
188            for ( int i = 0; i < fromFields.length; i++ ) {
189                int resultPos = resultPosMap.get( fromFields[i] );
190                Object keyValue = resultValues[resultPos];
191                if ( keyValue == null ) {
192                    return null;
193                }
194                query.append( tableAliases[0] );
195                query.append( '.' );
196                query.append( toFields[i].getField() );
197                query.append( "=?" );
198                query.addArgument( keyValue, toFields[i].getType() );
199                if ( i != fromFields.length - 1 ) {
200                    query.append( " AND " );
201                }
202            }
203            return query;
204        }
205    
206        /**
207         * Builds a SELECT statement to fetch the feature ids and the (concrete) feature types of feature properties that
208         * are stored in a related table (currently limited to *one* join table).
209         * <p>
210         * This is only necessary for feature properties that contain feature types with more than one possible
211         * substitution.
212         * 
213         * @param relation1
214         *            first table relation that leads to the join table
215         * @param relation2
216         *            second table relation that leads to the table where the property is stored
217         * @param resultValues
218         *            all retrieved columns from one result set row
219         * @param mappingFieldMap
220         *            key class: MappingField, value class: Integer (this is the associated index in resultValues)
221         * @return the statement or null if the keys in resultValues contain NULL values
222         */
223        private StatementBuffer buildFeatureTypeSelect( TableRelation relation1, TableRelation relation2,
224                                                        Object[] resultValues, Map mappingFieldMap ) {
225            StatementBuffer query = new StatementBuffer();
226            query.append( "SELECT " );
227            // append feature type column
228            query.append( FT_COLUMN );
229            // append feature id columns
230            MappingField[] fidFields = relation2.getFromFields();
231            for ( int i = 0; i < fidFields.length; i++ ) {
232                query.append( ',' );
233                query.append( fidFields[i].getField() );
234            }
235            query.append( " FROM " );
236            query.append( relation1.getToTable() );
237            query.append( " WHERE " );
238            // append key constraints
239            MappingField[] fromFields = relation1.getFromFields();
240            MappingField[] toFields = relation1.getToFields();
241            for ( int i = 0; i < fromFields.length; i++ ) {
242                Integer resultPos = (Integer) mappingFieldMap.get( fromFields[i] );
243                Object keyValue = resultValues[resultPos.intValue()];
244                if ( keyValue == null ) {
245                    return null;
246                }
247                query.append( toFields[i].getField() );
248                query.append( "=?" );
249                query.addArgument( keyValue, toFields[i].getType() );
250                if ( i != fromFields.length - 1 ) {
251                    query.append( " AND " );
252                }
253            }
254            return query;
255        }
256    
257        /**
258         * Builds a SELECT statement to fetch the feature id and the (concrete) feature type of a feature property that is
259         * stored in a related table (with the fk in the current table).
260         * <p>
261         * This is only necessary for feature properties that contain feature types with more than one possible
262         * substitution.
263         * 
264         * TODO: Select the FT_ column beforehand.
265         * 
266         * @param relation
267         *            table relation that leads to the subfeature table
268         * @param resultValues
269         *            all retrieved columns from one result set row
270         * @param mappingFieldMap
271         *            key class: MappingField, value class: Integer (this is the associated index in resultValues)
272         * @return the statement or null if the keys in resultValues contain NULL values
273         */
274        private StatementBuffer buildFeatureTypeSelect( TableRelation relation, Object[] resultValues, Map mappingFieldMap ) {
275            StatementBuffer query = new StatementBuffer();
276            query.append( "SELECT DISTINCT " );
277            // append feature type column
278            query.append( FT_PREFIX + relation.getFromFields()[0].getField() );
279            // append feature id columns
280            MappingField[] fidFields = relation.getFromFields();
281            for ( int i = 0; i < fidFields.length; i++ ) {
282                query.append( ',' );
283                query.append( fidFields[i].getField() );
284            }
285            query.append( " FROM " );
286            query.append( relation.getFromTable() );
287            query.append( " WHERE " );
288            // append key constraints
289            MappingField[] fromFields = relation.getFromFields();
290            for ( int i = 0; i < fromFields.length; i++ ) {
291                Integer resultPos = (Integer) mappingFieldMap.get( fromFields[i] );
292                Object keyValue = resultValues[resultPos.intValue()];
293                if ( keyValue == null ) {
294                    return null;
295                }
296                query.append( fromFields[i].getField() );
297                query.append( "=?" );
298                query.addArgument( keyValue, fromFields[i].getType() );
299                if ( i != fromFields.length - 1 ) {
300                    query.append( " AND " );
301                }
302            }
303            return query;
304        }
305    
306        /**
307         * Builds a SELECT statement that fetches one feature and it's properties.
308         * 
309         * @param fid
310         *            id of the feature to fetch
311         * @param table
312         *            root table of the feature
313         * @param fetchContents
314         * @return the statement or null if the keys in resultValues contain NULL values
315         */
316        private StatementBuffer buildFeatureSelect( FeatureId fid, String table, List<List<SimpleContent>> fetchContents ) {
317    
318            StatementBuffer query = new StatementBuffer();
319            query.append( "SELECT " );
320            appendQualifiedContentList( query, table, fetchContents );
321            query.append( " FROM " );
322            query.append( table );
323            query.append( " WHERE " );
324    
325            // append feature id constraints
326            MappingField[] fidFields = fid.getFidDefinition().getIdFields();
327            for ( int i = 0; i < fidFields.length; i++ ) {
328                query.append( fidFields[i].getField() );
329                query.append( "=?" );
330                query.addArgument( fid.getValue( i ), fidFields[i].getType() );
331                if ( i != fidFields.length - 1 ) {
332                    query.append( " AND " );
333                }
334            }
335            return query;
336        }
337    
338        /**
339         * Extracts a feature from the values of a result set row.
340         * 
341         * @param fid
342         *            feature id of the feature
343         * @param requestedPropertyMap
344         *            requested <code>MappedPropertyType</code>s mapped to <code>Collection</code> of
345         *            <code>PropertyPath</code>s
346         * @param resultPosMap
347         *            key class: MappingField, value class: Integer (this is the associated index in resultValues)
348         * @param resultValues
349         *            all retrieved columns from one result set row
350         * @return the extracted feature
351         * @throws SQLException
352         *             if a JDBC related error occurs
353         * @throws DatastoreException
354         * @throws UnknownCRSException
355         */
356        protected Feature extractFeature( FeatureId fid,
357                                          Map<MappedPropertyType, Collection<PropertyPath>> requestedPropertyMap,
358                                          Map<SimpleContent, Integer> resultPosMap, Object[] resultValues )
359                                throws SQLException, DatastoreException, UnknownCRSException {
360    
361            LOG.logDebug( "id = " + fid.getAsString() );
362    
363            this.featuresInGeneration.add( fid );
364    
365            // extract the requested properties of the feature
366            List<FeatureProperty> propertyList = new ArrayList<FeatureProperty>();
367            for ( MappedPropertyType requestedProperty : requestedPropertyMap.keySet() ) {
368                Collection<PropertyPath> propertyPaths = requestedPropertyMap.get( requestedProperty );
369                // PropertyPath[] requestingPaths = PropertyPathResolver.determineSubPropertyPaths
370                // (requestedProperty, propertyPaths);
371                Collection<FeatureProperty> props = extractProperties( requestedProperty, propertyPaths, resultPosMap,
372                                                                       resultValues );
373                propertyList.addAll( props );
374            }
375            FeatureProperty[] properties = propertyList.toArray( new FeatureProperty[propertyList.size()] );
376            Feature feature = FeatureFactory.createFeature( fid.getAsString(), fid.getFeatureType(), properties );
377    
378            this.featureMap.put( fid, feature );
379            return feature;
380        }
381    
382        /**
383         * Extracts the feature id from the values of a result set row.
384         * 
385         * @param ft
386         *            feature type for which the id shall be extracted
387         * @param mfMap
388         *            key class: MappingField, value class: Integer (this is the associated index in resultValues)
389         * @param resultValues
390         *            all retrieved columns from one result set row
391         * @return the feature id
392         * @throws DatastoreException
393         */
394        protected FeatureId extractFeatureId( MappedFeatureType ft, Map<SimpleContent, Integer> mfMap, Object[] resultValues )
395                                throws DatastoreException {
396            MappingField[] idFields = ft.getGMLId().getIdFields();
397            Object[] idValues = new Object[idFields.length];
398            for ( int i = 0; i < idFields.length; i++ ) {
399                Integer resultPos = mfMap.get( idFields[i] );
400                Object idValue = resultValues[resultPos.intValue()];
401                if ( idValue == null ) {
402                    String msg = Messages.getMessage( "DATASTORE_FEATURE_ID_NULL", ft.getTable(), ft.getName(),
403                                                      idFields[i].getField() );
404                    throw new DatastoreException( msg );
405                }
406                idValues[i] = idValue;
407            }
408            return new FeatureId( ft, idValues );
409        }
410    
411        /**
412         * Extracts the properties of the given property type from the values of a result set row. If the property is stored
413         * in related table, only the key values are present in the result set row and more SELECTs are built and executed
414         * to build the property.
415         * <p>
416         * NOTE: If the property is not stored in a related table, only one FeatureProperty is returned, otherwise the
417         * number of properties depends on the multiplicity of the relation.
418         * 
419         * @param pt
420         *            the mapped property type to be extracted
421         * @param propertyPaths
422         *            property paths that refer to the property to be extracted
423         * @param resultPosMap
424         *            key class: SimpleContent, value class: Integer (this is the associated index in resultValues)
425         * @param resultValues
426         *            all retrieved columns from one result set row
427         * @return Collection of FeatureProperty instances
428         * @throws SQLException
429         *             if a JDBC related error occurs
430         * @throws DatastoreException
431         * @throws UnknownCRSException
432         */
433        private Collection<FeatureProperty> extractProperties( MappedPropertyType pt,
434                                                               Collection<PropertyPath> propertyPaths,
435                                                               Map<SimpleContent, Integer> resultPosMap,
436                                                               Object[] resultValues )
437                                throws SQLException, DatastoreException, UnknownCRSException {
438    
439            Collection<FeatureProperty> result = null;
440    
441            TableRelation[] tableRelations = pt.getTableRelations();
442            if ( tableRelations != null && tableRelations.length != 0 ) {
443                LOG.logDebug( "Fetching related properties: '" + pt.getName() + "'..." );
444                result = fetchRelatedProperties( pt.getName(), pt, propertyPaths, resultPosMap, resultValues );
445            } else {
446                Object propertyValue = null;
447                if ( pt instanceof MappedSimplePropertyType ) {
448                    SimpleContent content = ( (MappedSimplePropertyType) pt ).getContent();
449                    if ( content instanceof MappingField ) {
450                        Integer resultPos = resultPosMap.get( content );
451                        propertyValue = datastore.convertFromDBType( resultValues[resultPos.intValue()],
452                                                                                       pt.getType() );
453                    } else if ( content instanceof ConstantContent ) {
454                        propertyValue = ( (ConstantContent) content ).getValue();
455                    } else if ( content instanceof SQLFunctionCall ) {
456                        Integer resultPos = resultPosMap.get( content );
457                        propertyValue = resultValues[resultPos.intValue()];
458                    }
459                } else if ( pt instanceof MappedGeometryPropertyType ) {
460                    MappingGeometryField field = ( (MappedGeometryPropertyType) pt ).getMappingField();
461                    Integer resultPos = null;
462                    SQLFunctionCall transformCall = this.fieldToTransformCall.get( field );
463                    if ( transformCall != null ) {
464                        resultPos = resultPosMap.get( transformCall );
465                    } else {
466                        resultPos = resultPosMap.get( field );
467                    }
468                    propertyValue = resultValues[resultPos.intValue()];
469    
470                    CoordinateSystem cs = ( (MappedGeometryPropertyType) pt ).getCS();
471                    if ( this.queryCS != null ) {
472                        cs = this.queryCS;
473                    }
474                    propertyValue = this.datastore.convertDBToDeegreeGeometry( propertyValue, cs, conn );
475                } else {
476                    String msg = "Unsupported property type: '" + pt.getClass().getName()
477                                 + "' in QueryHandler.extractProperties(). ";
478                    LOG.logError( msg );
479                    throw new IllegalArgumentException( msg );
480                }
481                FeatureProperty property = FeatureFactory.createFeatureProperty( pt.getName(), propertyValue );
482                result = new ArrayList<FeatureProperty>();
483                result.add( property );
484            }
485            return result;
486        }
487    
488        /**
489         * Extracts a {@link FeatureId} from one result set row.
490         * 
491         * @param ft
492         * @param rs
493         * @param startIndex
494         * @return feature id from result set row
495         * @throws SQLException
496         */
497        private FeatureId extractFeatureId( MappedFeatureType ft, ResultSet rs, int startIndex )
498                                throws SQLException {
499            MappedGMLId gmlId = ft.getGMLId();
500            MappingField[] idFields = gmlId.getIdFields();
501    
502            Object[] idValues = new Object[idFields.length];
503            for ( int i = 0; i < idValues.length; i++ ) {
504                idValues[i] = rs.getObject( i + startIndex );
505            }
506            return new FeatureId( ft, idValues );
507        }
508    
509        /**
510         * Determines the columns / functions that have to be fetched from the table of the given {@link MappedFeatureType}
511         * and associates identical columns / functions to avoid that the same column / function is SELECTed more than once.
512         * Identical columns are put into the same (inner) list.
513         * <p>
514         * The following {@link SimpleContent} instances of the {@link MappedFeatureType}s annotation are used to build the
515         * list:
516         * <ul>
517         * <li>MappingFields from the wfs:gmlId - annotation element of the feature type definition</li>
518         * <li>MappingFields in the annotations of the property element definitions; if the property's content is stored in
519         * a related table, the MappingFields used in the first wfs:Relation element's wfs:From element are considered</li>
520         * <li>SQLFunctionCalls in the annotations of the property element definitions</li>
521         * </ul>
522         * 
523         * @param ft
524         *            feature type for which the content list is built
525         * @param requestedProps
526         *            requested properties
527         * @return List of Lists (that contains SimpleContent instance that refer the same column)
528         * @throws DatastoreException
529         */
530        protected List<List<SimpleContent>> determineFetchContents( MappedFeatureType ft, PropertyType[] requestedProps )
531                                throws DatastoreException {
532    
533            List<List<SimpleContent>> fetchList = new ArrayList<List<SimpleContent>>();
534    
535            // helper lookup map (column names -> referencing MappingField instances)
536            Map<String, List<SimpleContent>> columnsMap = new HashMap<String, List<SimpleContent>>();
537    
538            // add table columns that are necessary to build the feature's gml id
539            MappingField[] idFields = ft.getGMLId().getIdFields();
540            for ( int i = 0; i < idFields.length; i++ ) {
541                List<SimpleContent> mappingFieldList = columnsMap.get( idFields[i].getField() );
542                if ( mappingFieldList == null ) {
543                    mappingFieldList = new ArrayList<SimpleContent>();
544                }
545                mappingFieldList.add( idFields[i] );
546                columnsMap.put( idFields[i].getField(), mappingFieldList );
547            }
548    
549            // add columns that are necessary to build the requested feature properties
550            for ( int i = 0; i < requestedProps.length; i++ ) {
551                MappedPropertyType pt = (MappedPropertyType) requestedProps[i];
552                TableRelation[] tableRelations = pt.getTableRelations();
553                if ( tableRelations != null && tableRelations.length != 0 ) {
554                    // if property is not stored in feature type's table, retrieve key fields of
555                    // the first relation's 'From' element
556                    MappingField[] fields = tableRelations[0].getFromFields();
557                    for ( int k = 0; k < fields.length; k++ ) {
558                        List<SimpleContent> list = columnsMap.get( fields[k].getField() );
559                        if ( list == null ) {
560                            list = new ArrayList<SimpleContent>();
561                        }
562                        list.add( fields[k] );
563                        columnsMap.put( fields[k].getField(), list );
564                    }
565                    // if (content instanceof FeaturePropertyContent) {
566                    // if (tableRelations.length == 1) {
567                    // // if feature property contains an abstract feature type, retrieve
568                    // // feature type as well (stored in column named "FT_fk")
569                    // MappedFeatureType subFeatureType = ( (FeaturePropertyContent) content )
570                    // .getFeatureTypeReference().getFeatureType();
571                    // if (subFeatureType.isAbstract()) {
572                    // String typeColumn = FT_PREFIX + fields [0].getField();
573                    // columnsMap.put (typeColumn, new ArrayList ());
574                    // }
575                    // }
576                    // }
577                } else {
578                    String column = null;
579                    SimpleContent content = null;
580                    if ( pt instanceof MappedSimplePropertyType ) {
581                        content = ( (MappedSimplePropertyType) pt ).getContent();
582                        if ( content instanceof MappingField ) {
583                            column = ( (MappingField) content ).getField();
584                        } else {
585                            // ignore virtual properties here (handled below)
586                            continue;
587                        }
588                    } else if ( pt instanceof MappedGeometryPropertyType ) {
589                        content = determineFetchContent( (MappedGeometryPropertyType) pt );
590                        column = ( (MappedGeometryPropertyType) pt ).getMappingField().getField();
591                    } else {
592                        assert false;
593                    }
594                    List<SimpleContent> contentList = columnsMap.get( column );
595                    if ( contentList == null ) {
596                        contentList = new ArrayList<SimpleContent>();
597                    }
598                    contentList.add( content );
599                    columnsMap.put( column, contentList );
600                }
601            }
602    
603            fetchList.addAll( columnsMap.values() );
604    
605            // add functions that are necessary to build the requested feature properties
606            for ( int i = 0; i < requestedProps.length; i++ ) {
607                MappedPropertyType pt = (MappedPropertyType) requestedProps[i];
608                if ( pt instanceof MappedSimplePropertyType ) {
609                    SimpleContent content = ( (MappedSimplePropertyType) pt ).getContent();
610                    if ( content instanceof SQLFunctionCall ) {
611                        List<SimpleContent> functionCallList = new ArrayList<SimpleContent>( 1 );
612                        functionCallList.add( content );
613                        fetchList.add( functionCallList );
614                    } else {
615                        // ignore other content types here
616                        continue;
617                    }
618                }
619    
620            }
621            return fetchList;
622        }
623    
624        /**
625         * Determines a {@link SimpleContent} object that represents the queried {@link MappedGeometryProperty} in the
626         * requested SRS.
627         * <p>
628         * <ul>
629         * <li>If the query SRS is identical to the geometry field's SRS (and thus the SRS of the stored geometry, the
630         * corresponding {@link MappingGeometryField} is returned.</li>
631         * <li>If the query SRS differs from the geometry field's SRS (and thus the SRS of the stored geometry, an
632         * {@link SQLFunctionCall} is returned that refers to the stored geometry, but transforms it to the queried SRS.</li>
633         * </ul>
634         * 
635         * @param pt
636         *            geometry property
637         * @return a <code>SimpleContent</code> instance that represents the queried geometry property
638         * @throws DatastoreException
639         *             if the transform call cannot be generated
640         */
641        private SimpleContent determineFetchContent( MappedGeometryPropertyType pt )
642                                throws DatastoreException {
643    
644            MappingGeometryField field = pt.getMappingField();
645            SimpleContent content = field;
646    
647            String queriedSRS = this.datastore.checkTransformation( pt, this.query.getSrsName() );
648            if ( queriedSRS != null ) {
649                content = this.fieldToTransformCall.get( field );
650                if ( content == null ) {
651                    content = this.datastore.buildSRSTransformCall( pt, queriedSRS );
652                    this.fieldToTransformCall.put( field, (SQLFunctionCall) content );
653                }
654            }
655            return content;
656        }
657    
658        /**
659         * Retrieves the feature with the given feature id.
660         * 
661         * @param fid
662         * @param requestedPaths
663         * @return the feature with the given type and feature id, may be null
664         * @throws SQLException
665         * @throws DatastoreException
666         * @throws UnknownCRSException
667         */
668        private Feature fetchFeature( FeatureId fid, PropertyPath[] requestedPaths )
669                                throws SQLException, DatastoreException, UnknownCRSException {
670    
671            Feature feature = null;
672            MappedFeatureType ft = fid.getFeatureType();
673            // TODO what about aliases here?
674            Map<MappedPropertyType, Collection<PropertyPath>> requestedPropMap = PropertyPathResolver.determineFetchProperties(
675                                                                                                                                ft,
676                                                                                                                                null,
677                                                                                                                                requestedPaths );
678            MappedPropertyType[] requestedProps = requestedPropMap.keySet().toArray(
679                                                                                     new MappedPropertyType[requestedPropMap.size()] );
680    
681            if ( requestedProps.length > 0 ) {
682    
683                // determine contents (fields / functions) that must be SELECTed from root table
684                List<List<SimpleContent>> fetchContents = determineFetchContents( ft, requestedProps );
685                Map<SimpleContent, Integer> resultPosMap = buildResultPosMap( fetchContents );
686    
687                // build feature query
688                StatementBuffer query = buildFeatureSelect( fid, ft.getTable(), fetchContents );
689                LOG.logDebug( "Feature query: '" + query + "'" );
690                Object[] resultValues = new Object[fetchContents.size()];
691                PreparedStatement stmt = null;
692                ResultSet rs = null;
693                try {
694                    stmt = this.datastore.prepareStatement( this.conn, query );
695                    rs = stmt.executeQuery();
696    
697                    if ( rs.next() ) {
698                        // collect result values
699                        for ( int i = 0; i < resultValues.length; i++ ) {
700                            resultValues[i] = rs.getObject( i + 1 );
701                        }
702                        feature = extractFeature( fid, requestedPropMap, resultPosMap, resultValues );
703                    } else {
704                        String msg = Messages.getMessage( "DATASTORE_FEATURE_QUERY_NO_RESULT", query.getQueryString() );
705                        LOG.logError( msg );
706                        throw new DatastoreException( msg );
707                    }
708                    if ( rs.next() ) {
709                        String msg = Messages.getMessage( "DATASTORE_FEATURE_QUERY_MORE_THAN_ONE_RESULT",
710                                                          query.getQueryString() );
711                        LOG.logError( msg );
712                        throw new DatastoreException( msg );
713                    }
714                } finally {
715                    try {
716                        if ( rs != null ) {
717                            rs.close();
718                        }
719                    } finally {
720                        if ( stmt != null ) {
721                            stmt.close();
722                        }
723                    }
724                }
725            }
726            return feature;
727        }
728    
729        /**
730         * 
731         * @param propertyName
732         * @param pt
733         * @param propertyPaths
734         *            property paths that refer to the property to be extracted
735         * @param resultPosMap
736         *            key class: MappingField, value class: Integer (this is the associated index in resultValues)
737         * @param resultValues
738         *            all retrieved columns from one result set row
739         * @return Collection of FeatureProperty instances
740         * @throws SQLException
741         *             if a JDBC related error occurs
742         * @throws DatastoreException
743         * @throws UnknownCRSException
744         */
745        private Collection<FeatureProperty> fetchRelatedProperties( QualifiedName propertyName, MappedPropertyType pt,
746                                                                    Collection<PropertyPath> propertyPaths,
747                                                                    Map<SimpleContent, Integer> resultPosMap,
748                                                                    Object[] resultValues )
749                                throws SQLException, DatastoreException, UnknownCRSException {
750    
751            Collection<FeatureProperty> result = new ArrayList<FeatureProperty>( 100 );
752            PreparedStatement stmt = null;
753            ResultSet rs = null;
754            try {
755                if ( pt instanceof MappedSimplePropertyType ) {
756                    SimpleContent content = ( (MappedSimplePropertyType) pt ).getContent();
757    
758                    // TODO check for invalid content types
759                    List<SimpleContent> fetchContents = new ArrayList<SimpleContent>( 1 );
760                    List<List<SimpleContent>> fetchContentsList = new ArrayList<List<SimpleContent>>( 1 );
761                    fetchContents.add( content );
762                    fetchContentsList.add( fetchContents );
763    
764                    StatementBuffer query = buildSubsequentSelect( fetchContentsList, pt.getTableRelations(), resultValues,
765                                                                   resultPosMap );
766                    LOG.logDebug( "Subsequent query: '" + query + "'" );
767                    if ( query != null ) {
768                        stmt = this.datastore.prepareStatement( this.conn, query );
769                        rs = stmt.executeQuery();
770                        while ( rs.next() ) {
771                            Object propertyValue = datastore.convertFromDBType( rs.getObject( 1 ),
772                                                                                                  pt.getType() );
773                            FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName, propertyValue );
774                            result.add( property );
775                        }
776                    }
777    
778                } else if ( pt instanceof MappedGeometryPropertyType ) {
779                    SimpleContent content = ( (MappedGeometryPropertyType) pt ).getMappingField();
780                    CoordinateSystem cs = ( (MappedGeometryPropertyType) pt ).getCS();
781    
782                    if ( this.queryCS != null ) {
783                        cs = this.queryCS;
784                    }
785                    content = determineFetchContent( (MappedGeometryPropertyType) pt );
786    
787                    List<SimpleContent> fetchContents = new ArrayList<SimpleContent>( 1 );
788                    List<List<SimpleContent>> fetchContentsList = new ArrayList<List<SimpleContent>>( 1 );
789                    fetchContents.add( content );
790                    fetchContentsList.add( fetchContents );
791    
792                    StatementBuffer query = buildSubsequentSelect( fetchContentsList, pt.getTableRelations(), resultValues,
793                                                                   resultPosMap );
794                    LOG.logDebug( "Subsequent query: '" + query + "'" );
795                    if ( query != null ) {
796                        stmt = this.datastore.prepareStatement( this.conn, query );
797                        rs = stmt.executeQuery();
798                        while ( rs.next() ) {
799                            Object value = rs.getObject( 1 );
800                            Geometry geometry = this.datastore.convertDBToDeegreeGeometry( value, cs, this.conn );
801                            FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName, geometry );
802                            result.add( property );
803                        }
804                    }
805                } else if ( pt instanceof MappedFeaturePropertyType ) {
806                    MappedFeatureType ft = ( (MappedFeaturePropertyType) pt ).getFeatureTypeReference().getFeatureType();
807                    MappedFeatureType[] substitutions = ft.getConcreteSubstitutions();
808                    if ( substitutions.length > 1 ) {
809                        // if feature type has more than one concrete substitution, determine concrete
810                        // feature type first
811                        String msg = StringTools.concat( 200, "FeatureType '", ft.getName(),
812                                                         "' has more than one concrete ",
813                                                         "substitution. Need to determine ", "feature type table first." );
814                        LOG.logDebug( msg );
815                        LOG.logDebug( "Property: " + pt.getName() );
816                        TableRelation[] tableRelations = pt.getTableRelations();
817                        if ( tableRelations.length == 2 ) {
818                            StatementBuffer query = buildFeatureTypeSelect( tableRelations[0], tableRelations[1],
819                                                                            resultValues, resultPosMap );
820                            LOG.logDebug( "Feature type (and id) query: '" + query + "'" );
821                            if ( query != null ) {
822                                stmt = this.datastore.prepareStatement( this.conn, query );
823                                rs = stmt.executeQuery();
824                                while ( rs.next() ) {
825                                    String featureTypeName = rs.getString( 1 );
826                                    MappedFeatureType concreteFeatureType = ft.getGMLSchema().getFeatureType(
827                                                                                                              featureTypeName );
828                                    if ( concreteFeatureType == null ) {
829                                        msg = StringTools.concat( 200, "Lookup of concrete feature type '",
830                                                                  featureTypeName, "' failed: ",
831                                                                  " Inconsistent featuretype column!?" );
832                                        LOG.logError( msg );
833                                        throw new DatastoreException( msg );
834                                    }
835                                    FeatureId fid = extractFeatureId( concreteFeatureType, rs, 2 );
836                                    msg = StringTools.concat( 200, "Subfeature '", fid.getAsString(),
837                                                              "' has concrete feature type '",
838                                                              concreteFeatureType.getName(), "'." );
839                                    LOG.logDebug( msg );
840    
841                                    if ( !this.featuresInGeneration.contains( fid ) ) {
842                                        PropertyPath[] subPropertyPaths = PropertyPathResolver.determineSubPropertyPaths(
843                                                                                                                          concreteFeatureType,
844                                                                                                                          propertyPaths );
845                                        Feature feature = fetchFeature( fid, subPropertyPaths );
846                                        if ( feature != null ) {
847                                            FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName,
848                                                                                                             feature );
849                                            result.add( property );
850                                        }
851                                    } else {
852                                        FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName, null );
853                                        addToFidToPropertyMap( fid, property );
854                                        result.add( property );
855                                    }
856                                }
857                            }
858                        } else if ( tableRelations.length == 1 ) {
859                            StatementBuffer query = buildFeatureTypeSelect( tableRelations[0], resultValues, resultPosMap );
860                            LOG.logDebug( "Feature type (and id) query: '" + query + "'" );
861                            if ( query != null ) {
862                                stmt = this.datastore.prepareStatement( this.conn, query );
863                                rs = stmt.executeQuery();
864                                while ( rs.next() ) {
865                                    String featureTypeName = rs.getString( 1 );
866                                    MappedFeatureType concreteFeatureType = ft.getGMLSchema().getFeatureType(
867                                                                                                              featureTypeName );
868                                    if ( concreteFeatureType == null ) {
869                                        msg = StringTools.concat( 200, "Lookup of concrete feature type '",
870                                                                  featureTypeName, "' failed: ",
871                                                                  " Inconsistent featuretype column!?" );
872                                        LOG.logError( msg );
873                                        throw new DatastoreException( msg );
874                                    }
875    
876                                    FeatureId fid = extractFeatureId( concreteFeatureType, rs, 2 );
877    
878                                    msg = StringTools.concat( 200, "Subfeature '", fid.getAsString(),
879                                                              "' has concrete feature type '",
880                                                              concreteFeatureType.getName(), "'." );
881                                    LOG.logDebug( msg );
882    
883                                    FeatureProperty property = null;
884                                    if ( !this.featuresInGeneration.contains( fid ) ) {
885                                        PropertyPath[] subPropertyPaths = PropertyPathResolver.determineSubPropertyPaths(
886                                                                                                                          concreteFeatureType,
887                                                                                                                          propertyPaths );
888                                        Feature feature = fetchFeature( fid, subPropertyPaths );
889                                        if ( feature != null ) {
890                                            property = FeatureFactory.createFeatureProperty( propertyName, feature );
891                                            result.add( property );
892                                        }
893    
894                                    } else {
895                                        property = FeatureFactory.createFeatureProperty( propertyName, null );
896                                        addToFidToPropertyMap( fid, property );
897                                        result.add( property );
898                                    }
899                                }
900                            }
901                        } else {
902                            msg = StringTools.concat( 200, "Querying of feature properties ",
903                                                      "with a content type with more than one ",
904                                                      "concrete substitution is not implemented for ",
905                                                      tableRelations.length, " TableRelations." );
906                            throw new DatastoreException( msg );
907                        }
908                    } else {
909                        // feature type is the only substitutable concrete feature type
910                        PropertyPath[] subPropertyPaths = PropertyPathResolver.determineSubPropertyPaths( ft, propertyPaths );
911                        // TODO aliases?
912                        Map<MappedPropertyType, Collection<PropertyPath>> requestedPropertiesMap = PropertyPathResolver.determineFetchProperties(
913                                                                                                                                                  ft,
914                                                                                                                                                  null,
915                                                                                                                                                  subPropertyPaths );
916                        MappedPropertyType[] requestedProperties = requestedPropertiesMap.keySet().toArray(
917                                                                                                            new MappedPropertyType[requestedPropertiesMap.size()] );
918    
919                        // determine contents (fields / functions) that needs to be SELECTed from
920                        // current table
921                        List<List<SimpleContent>> fetchContents = determineFetchContents( ft, requestedProperties );
922                        Map<SimpleContent, Integer> newResultPosMap = buildResultPosMap( fetchContents );
923    
924                        StatementBuffer query = buildSubsequentSelect( fetchContents, pt.getTableRelations(), resultValues,
925                                                                       resultPosMap );
926                        LOG.logDebug( "Subsequent query: '" + query + "'" );
927    
928                        if ( query != null ) {
929                            Object[] newResultValues = new Object[fetchContents.size()];
930                            stmt = this.datastore.prepareStatement( this.conn, query );
931                            rs = stmt.executeQuery();
932                            while ( rs.next() ) {
933                                // cache result values
934                                for ( int i = 0; i < newResultValues.length; i++ ) {
935                                    newResultValues[i] = rs.getObject( i + 1 );
936                                }
937                                FeatureId fid = extractFeatureId( ft, newResultPosMap, newResultValues );
938                                FeatureProperty property = null;
939                                if ( !this.featuresInGeneration.contains( fid ) ) {
940                                    Feature feature = extractFeature( fid, requestedPropertiesMap, newResultPosMap,
941                                                                      newResultValues );
942                                    property = FeatureFactory.createFeatureProperty( propertyName, feature );
943                                } else {
944                                    property = FeatureFactory.createFeatureProperty( propertyName, null );
945                                    addToFidToPropertyMap( fid, property );
946                                }
947                                result.add( property );
948                            }
949                        }
950                    }
951                } else {
952                    String msg = "Unsupported content type: '" + pt.getClass().getName()
953                                 + "' in QueryHandler.fetchRelatedProperties().";
954                    throw new IllegalArgumentException( msg );
955                }
956            } finally {
957                try {
958                    if ( rs != null ) {
959                        rs.close();
960                    }
961                    if ( stmt != null ) {
962                        stmt.close();
963                    }
964                } finally {
965                    if ( stmt != null ) {
966                        stmt.close();
967                    }
968                }
969            }
970            return result;
971        }
972    
973        private void addToFidToPropertyMap( FeatureId fid, FeatureProperty property ) {
974            List<FeatureProperty> properties = this.fidToPropertyMap.get( fid );
975            if ( properties == null ) {
976                properties = new ArrayList<FeatureProperty>();
977                this.fidToPropertyMap.put( fid, properties );
978            }
979            properties.add( property );
980        }
981    
982        protected void appendQualifiedContentList( StatementBuffer query, String tableAlias,
983                                                   List<List<SimpleContent>> fetchContents ) {
984    
985            for ( int i = 0; i < fetchContents.size(); i++ ) {
986                SimpleContent content = fetchContents.get( i ).get( 0 );
987                if ( content instanceof MappingField ) {
988                    appendQualifiedColumn( query, tableAlias, ( (MappingField) content ).getField() );
989                } else if ( content instanceof SQLFunctionCall ) {
990                    this.vcProvider.appendSQLFunctionCall( query, tableAlias, (SQLFunctionCall) content );
991                } else {
992                    assert false;
993                }
994                if ( i != fetchContents.size() - 1 ) {
995                    query.append( "," );
996                }
997            }
998        }
999    
1000        /**
1001         * Builds a lookup map that allows to find the index (position in the {@link ResultSet}) by the
1002         * {@link SimpleContent} instance that makes it necessary to fetch it.
1003         * 
1004         * @param fetchContents
1005         * @return key: SimpleContent, value: Integer (position in ResultSet)
1006         */
1007        protected Map<SimpleContent, Integer> buildResultPosMap( List<List<SimpleContent>> fetchContents ) {
1008    
1009            Map<SimpleContent, Integer> resultPosMap = new HashMap<SimpleContent, Integer>();
1010            for ( int i = 0; i < fetchContents.size(); i++ ) {
1011                for ( SimpleContent content : fetchContents.get( i ) ) {
1012                    resultPosMap.put( content, i );
1013                }
1014            }
1015            return resultPosMap;
1016        }
1017    }