001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/io/datastore/sql/FeatureFetcher.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2008 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: apoth $
098     * 
099     * @version $Revision: 9342 $, $Date: 2007-12-27 13:32:57 +0100 (Do, 27 Dez 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()], pt.getType() );
452                    } else if ( content instanceof ConstantContent ) {
453                        propertyValue = ( (ConstantContent) content ).getValue();
454                    } else if ( content instanceof SQLFunctionCall ) {
455                        Integer resultPos = resultPosMap.get( content );
456                        propertyValue = resultValues[resultPos.intValue()];
457                    }
458                } else if ( pt instanceof MappedGeometryPropertyType ) {
459                    MappingGeometryField field = ( (MappedGeometryPropertyType) pt ).getMappingField();
460                    Integer resultPos = null;
461                    SQLFunctionCall transformCall = this.fieldToTransformCall.get( field );
462                    if ( transformCall != null ) {
463                        resultPos = resultPosMap.get( transformCall );
464                    } else {
465                        resultPos = resultPosMap.get( field );
466                    }
467                    propertyValue = resultValues[resultPos.intValue()];
468    
469                    CoordinateSystem cs = ( (MappedGeometryPropertyType) pt ).getCS();
470                    if ( this.queryCS != null ) {
471                        cs = this.queryCS;
472                    }
473                    propertyValue = this.datastore.convertDBToDeegreeGeometry( propertyValue, cs, conn );
474                } else {
475                    String msg = "Unsupported property type: '" + pt.getClass().getName()
476                                 + "' in QueryHandler.extractProperties(). ";
477                    LOG.logError( msg );
478                    throw new IllegalArgumentException( msg );
479                }
480                FeatureProperty property = FeatureFactory.createFeatureProperty( pt.getName(), propertyValue );
481                result = new ArrayList<FeatureProperty>();
482                result.add( property );
483            }
484            return result;
485        }
486    
487        /**
488         * Extracts a {@link FeatureId} from one result set row.
489         * 
490         * @param ft
491         * @param rs
492         * @param startIndex
493         * @return feature id from result set row
494         * @throws SQLException
495         */
496        private FeatureId extractFeatureId( MappedFeatureType ft, ResultSet rs, int startIndex )
497                                throws SQLException {
498            MappedGMLId gmlId = ft.getGMLId();
499            MappingField[] idFields = gmlId.getIdFields();
500    
501            Object[] idValues = new Object[idFields.length];
502            for ( int i = 0; i < idValues.length; i++ ) {
503                idValues[i] = rs.getObject( i + startIndex );
504            }
505            return new FeatureId( ft, idValues );
506        }
507    
508        /**
509         * Determines the columns / functions that have to be fetched from the table of the given {@link MappedFeatureType}
510         * and associates identical columns / functions to avoid that the same column / function is SELECTed more than once.
511         * <p>
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; if the property's (derived) content
521         * is stored in a related table, the MappingFields used in the first wfs:Relation element's wfs:From element are
522         * considered</li>
523         * </ul>
524         * 
525         * @param ft
526         *            feature type for which the content list is built
527         * @param requestedProps
528         *            requested properties
529         * @return List of Lists (that contains SimpleContent instance that refer the same column)
530         * @throws DatastoreException
531         */
532        protected List<List<SimpleContent>> determineFetchContents( MappedFeatureType ft, PropertyType[] requestedProps )
533                                throws DatastoreException {
534    
535            List<List<SimpleContent>> fetchList = new ArrayList<List<SimpleContent>>();
536    
537            // helper lookup map (column names -> referencing MappingField instances)
538            Map<String, List<SimpleContent>> columnsMap = new HashMap<String, List<SimpleContent>>();
539    
540            // add table columns that are necessary to build the feature's gml id
541            MappingField[] idFields = ft.getGMLId().getIdFields();
542            for ( int i = 0; i < idFields.length; i++ ) {
543                List<SimpleContent> mappingFieldList = columnsMap.get( idFields[i].getField() );
544                if ( mappingFieldList == null ) {
545                    mappingFieldList = new ArrayList<SimpleContent>();
546                }
547                mappingFieldList.add( idFields[i] );
548                columnsMap.put( idFields[i].getField(), mappingFieldList );
549            }
550    
551            // add columns that are necessary to build the requested feature properties
552            for ( int i = 0; i < requestedProps.length; i++ ) {
553                MappedPropertyType pt = (MappedPropertyType) requestedProps[i];
554                TableRelation[] tableRelations = pt.getTableRelations();
555                if ( tableRelations != null && tableRelations.length != 0 ) {
556                    // if property is not stored in feature type's table, retrieve key fields of
557                    // the first relation's 'From' element
558                    MappingField[] fields = tableRelations[0].getFromFields();
559                    for ( int k = 0; k < fields.length; k++ ) {
560                        List<SimpleContent> list = columnsMap.get( fields[k].getField() );
561                        if ( list == null ) {
562                            list = new ArrayList<SimpleContent>();
563                        }
564                        list.add( fields[k] );
565                        columnsMap.put( fields[k].getField(), list );
566                    }
567                    // if (content instanceof FeaturePropertyContent) {
568                    // if (tableRelations.length == 1) {
569                    // // if feature property contains an abstract feature type, retrieve
570                    // // feature type as well (stored in column named "FT_fk")
571                    // MappedFeatureType subFeatureType = ( (FeaturePropertyContent) content )
572                    // .getFeatureTypeReference().getFeatureType();
573                    // if (subFeatureType.isAbstract()) {
574                    // String typeColumn = FT_PREFIX + fields [0].getField();
575                    // columnsMap.put (typeColumn, new ArrayList ());
576                    // }
577                    // }
578                    // }
579                } else {
580                    String column = null;
581                    SimpleContent content = null;
582                    if ( pt instanceof MappedSimplePropertyType ) {
583                        content = ( (MappedSimplePropertyType) pt ).getContent();
584                        if ( content instanceof MappingField ) {
585                            column = ( (MappingField) content ).getField();
586                        } else {
587                            // ignore virtual properties here (handled below)
588                            continue;
589                        }
590                    } else if ( pt instanceof MappedGeometryPropertyType ) {
591                        content = determineFetchContent( (MappedGeometryPropertyType) pt );
592                        column = ( (MappedGeometryPropertyType) pt ).getMappingField().getField();
593                    } else {
594                        assert false;
595                    }
596                    List<SimpleContent> contentList = columnsMap.get( column );
597                    if ( contentList == null ) {
598                        contentList = new ArrayList<SimpleContent>();
599                    }
600                    contentList.add( content );
601                    columnsMap.put( column, contentList );
602                }
603            }
604    
605            fetchList.addAll( columnsMap.values() );
606    
607            // add functions that are necessary to build the requested feature properties
608            for ( int i = 0; i < requestedProps.length; i++ ) {
609                MappedPropertyType pt = (MappedPropertyType) requestedProps[i];
610                TableRelation[] tableRelations = pt.getTableRelations();
611                if ( tableRelations == null || tableRelations.length == 0 ) {
612                    if ( pt instanceof MappedSimplePropertyType ) {
613                        SimpleContent content = ( (MappedSimplePropertyType) pt ).getContent();
614                        if ( content instanceof SQLFunctionCall ) {
615                            List<SimpleContent> functionCallList = new ArrayList<SimpleContent>( 1 );
616                            functionCallList.add( content );
617                            fetchList.add( functionCallList );
618                        } else {
619                            // ignore other content types here
620                            continue;
621                        }
622                    }
623                }
624            }
625            return fetchList;
626        }
627    
628        /**
629         * Determines a {@link SimpleContent} object that represents the queried {@link MappedGeometryProperty} in the
630         * requested SRS.
631         * <p>
632         * <ul>
633         * <li>If the query SRS is identical to the geometry field's SRS (and thus the SRS of the stored geometry, the
634         * corresponding {@link MappingGeometryField} is returned.</li>
635         * <li>If the query SRS differs from the geometry field's SRS (and thus the SRS of the stored geometry, an
636         * {@link SQLFunctionCall} is returned that refers to the stored geometry, but transforms it to the queried SRS.</li>
637         * </ul>
638         * 
639         * @param pt
640         *            geometry property
641         * @return a <code>SimpleContent</code> instance that represents the queried geometry property
642         * @throws DatastoreException
643         *             if the transform call cannot be generated
644         */
645        private SimpleContent determineFetchContent( MappedGeometryPropertyType pt )
646                                throws DatastoreException {
647    
648            MappingGeometryField field = pt.getMappingField();
649            SimpleContent content = field;
650    
651            String queriedSRS = this.datastore.checkTransformation( pt, this.query.getSrsName() );
652            if ( queriedSRS != null ) {
653                content = this.fieldToTransformCall.get( field );
654                if ( content == null ) {
655                    content = this.datastore.buildSRSTransformCall( pt, queriedSRS );
656                    this.fieldToTransformCall.put( field, (SQLFunctionCall) content );
657                }
658            }
659            return content;
660        }
661    
662        /**
663         * Retrieves the feature with the given feature id.
664         * 
665         * @param fid
666         * @param requestedPaths
667         * @return the feature with the given type and feature id, may be null
668         * @throws SQLException
669         * @throws DatastoreException
670         * @throws UnknownCRSException
671         */
672        private Feature fetchFeature( FeatureId fid, PropertyPath[] requestedPaths )
673                                throws SQLException, DatastoreException, UnknownCRSException {
674    
675            Feature feature = null;
676            MappedFeatureType ft = fid.getFeatureType();
677            // TODO what about aliases here?
678            Map<MappedPropertyType, Collection<PropertyPath>> requestedPropMap = PropertyPathResolver.determineFetchProperties(
679                                                                                                                                ft,
680                                                                                                                                null,
681                                                                                                                                requestedPaths );
682            MappedPropertyType[] requestedProps = requestedPropMap.keySet().toArray(
683                                                                                     new MappedPropertyType[requestedPropMap.size()] );
684    
685            if ( requestedProps.length > 0 ) {
686    
687                // determine contents (fields / functions) that must be SELECTed from root table
688                List<List<SimpleContent>> fetchContents = determineFetchContents( ft, requestedProps );
689                Map<SimpleContent, Integer> resultPosMap = buildResultPosMap( fetchContents );
690    
691                // build feature query
692                StatementBuffer query = buildFeatureSelect( fid, ft.getTable(), fetchContents );
693                LOG.logDebug( "Feature query: '" + query + "'" );
694                Object[] resultValues = new Object[fetchContents.size()];
695                PreparedStatement stmt = null;
696                ResultSet rs = null;
697                try {
698                    stmt = this.datastore.prepareStatement( this.conn, query );
699                    rs = stmt.executeQuery();
700    
701                    if ( rs.next() ) {
702                        // collect result values
703                        for ( int i = 0; i < resultValues.length; i++ ) {
704                            resultValues[i] = rs.getObject( i + 1 );
705                        }
706                        feature = extractFeature( fid, requestedPropMap, resultPosMap, resultValues );
707                    } else {
708                        String msg = Messages.getMessage( "DATASTORE_FEATURE_QUERY_NO_RESULT", query.getQueryString() );
709                        LOG.logError( msg );
710                        throw new DatastoreException( msg );
711                    }
712                    if ( rs.next() ) {
713                        String msg = Messages.getMessage( "DATASTORE_FEATURE_QUERY_MORE_THAN_ONE_RESULT",
714                                                          query.getQueryString() );
715                        LOG.logError( msg );
716                        throw new DatastoreException( msg );
717                    }
718                } finally {
719                    try {
720                        if ( rs != null ) {
721                            rs.close();
722                        }
723                    } finally {
724                        if ( stmt != null ) {
725                            stmt.close();
726                        }
727                    }
728                }
729            }
730            return feature;
731        }
732    
733        /**
734         * 
735         * @param propertyName
736         * @param pt
737         * @param propertyPaths
738         *            property paths that refer to the property to be extracted
739         * @param resultPosMap
740         *            key class: MappingField, value class: Integer (this is the associated index in resultValues)
741         * @param resultValues
742         *            all retrieved columns from one result set row
743         * @return Collection of FeatureProperty instances
744         * @throws SQLException
745         *             if a JDBC related error occurs
746         * @throws DatastoreException
747         * @throws UnknownCRSException
748         */
749        private Collection<FeatureProperty> fetchRelatedProperties( QualifiedName propertyName, MappedPropertyType pt,
750                                                                    Collection<PropertyPath> propertyPaths,
751                                                                    Map<SimpleContent, Integer> resultPosMap,
752                                                                    Object[] resultValues )
753                                throws SQLException, DatastoreException, UnknownCRSException {
754    
755            Collection<FeatureProperty> result = new ArrayList<FeatureProperty>( 100 );
756            PreparedStatement stmt = null;
757            ResultSet rs = null;
758            try {
759                if ( pt instanceof MappedSimplePropertyType ) {
760                    SimpleContent content = ( (MappedSimplePropertyType) pt ).getContent();
761    
762                    // TODO check for invalid content types
763                    List<SimpleContent> fetchContents = new ArrayList<SimpleContent>( 1 );
764                    List<List<SimpleContent>> fetchContentsList = new ArrayList<List<SimpleContent>>( 1 );
765                    fetchContents.add( content );
766                    fetchContentsList.add( fetchContents );
767    
768                    StatementBuffer query = buildSubsequentSelect( fetchContentsList, pt.getTableRelations(), resultValues,
769                                                                   resultPosMap );
770                    LOG.logDebug( "Subsequent query: '" + query + "'" );
771                    if ( query != null ) {
772                        stmt = this.datastore.prepareStatement( this.conn, query );
773                        rs = stmt.executeQuery();
774                        while ( rs.next() ) {
775                            Object propertyValue = datastore.convertFromDBType( rs.getObject( 1 ), pt.getType() );
776                            FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName, propertyValue );
777                            result.add( property );
778                        }
779                    }
780    
781                } else if ( pt instanceof MappedGeometryPropertyType ) {
782                    SimpleContent content = ( (MappedGeometryPropertyType) pt ).getMappingField();
783                    CoordinateSystem cs = ( (MappedGeometryPropertyType) pt ).getCS();
784    
785                    if ( this.queryCS != null ) {
786                        cs = this.queryCS;
787                    }
788                    content = determineFetchContent( (MappedGeometryPropertyType) pt );
789    
790                    List<SimpleContent> fetchContents = new ArrayList<SimpleContent>( 1 );
791                    List<List<SimpleContent>> fetchContentsList = new ArrayList<List<SimpleContent>>( 1 );
792                    fetchContents.add( content );
793                    fetchContentsList.add( fetchContents );
794    
795                    StatementBuffer query = buildSubsequentSelect( fetchContentsList, pt.getTableRelations(), resultValues,
796                                                                   resultPosMap );
797                    LOG.logDebug( "Subsequent query: '" + query + "'" );
798                    if ( query != null ) {
799                        stmt = this.datastore.prepareStatement( this.conn, query );
800                        rs = stmt.executeQuery();
801                        while ( rs.next() ) {
802                            Object value = rs.getObject( 1 );
803                            Geometry geometry = this.datastore.convertDBToDeegreeGeometry( value, cs, this.conn );
804                            FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName, geometry );
805                            result.add( property );
806                        }
807                    }
808                } else if ( pt instanceof MappedFeaturePropertyType ) {
809                    MappedFeatureType ft = ( (MappedFeaturePropertyType) pt ).getFeatureTypeReference().getFeatureType();
810                    MappedFeatureType[] substitutions = ft.getConcreteSubstitutions();
811                    if ( substitutions.length > 1 ) {
812                        // if feature type has more than one concrete substitution, determine concrete
813                        // feature type first
814                        String msg = StringTools.concat( 200, "FeatureType '", ft.getName(),
815                                                         "' has more than one concrete ",
816                                                         "substitution. Need to determine ", "feature type table first." );
817                        LOG.logDebug( msg );
818                        LOG.logDebug( "Property: " + pt.getName() );
819                        TableRelation[] tableRelations = pt.getTableRelations();
820                        if ( tableRelations.length == 2 ) {
821                            StatementBuffer query = buildFeatureTypeSelect( tableRelations[0], tableRelations[1],
822                                                                            resultValues, resultPosMap );
823                            LOG.logDebug( "Feature type (and id) query: '" + query + "'" );
824                            if ( query != null ) {
825                                stmt = this.datastore.prepareStatement( this.conn, query );
826                                rs = stmt.executeQuery();
827                                while ( rs.next() ) {
828                                    String featureTypeName = rs.getString( 1 );
829                                    MappedFeatureType concreteFeatureType = ft.getGMLSchema().getFeatureType(
830                                                                                                              featureTypeName );
831                                    if ( concreteFeatureType == null ) {
832                                        msg = StringTools.concat( 200, "Lookup of concrete feature type '",
833                                                                  featureTypeName, "' failed: ",
834                                                                  " Inconsistent featuretype column!?" );
835                                        LOG.logError( msg );
836                                        throw new DatastoreException( msg );
837                                    }
838                                    FeatureId fid = extractFeatureId( concreteFeatureType, rs, 2 );
839                                    msg = StringTools.concat( 200, "Subfeature '", fid.getAsString(),
840                                                              "' has concrete feature type '",
841                                                              concreteFeatureType.getName(), "'." );
842                                    LOG.logDebug( msg );
843    
844                                    if ( !this.featuresInGeneration.contains( fid ) ) {
845                                        PropertyPath[] subPropertyPaths = PropertyPathResolver.determineSubPropertyPaths(
846                                                                                                                          concreteFeatureType,
847                                                                                                                          propertyPaths );
848                                        Feature feature = fetchFeature( fid, subPropertyPaths );
849                                        if ( feature != null ) {
850                                            FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName,
851                                                                                                             feature );
852                                            result.add( property );
853                                        }
854                                    } else {
855                                        FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName, null );
856                                        addToFidToPropertyMap( fid, property );
857                                        result.add( property );
858                                    }
859                                }
860                            }
861                        } else if ( tableRelations.length == 1 ) {
862                            StatementBuffer query = buildFeatureTypeSelect( tableRelations[0], resultValues, resultPosMap );
863                            LOG.logDebug( "Feature type (and id) query: '" + query + "'" );
864                            if ( query != null ) {
865                                stmt = this.datastore.prepareStatement( this.conn, query );
866                                rs = stmt.executeQuery();
867                                while ( rs.next() ) {
868                                    String featureTypeName = rs.getString( 1 );
869                                    MappedFeatureType concreteFeatureType = ft.getGMLSchema().getFeatureType(
870                                                                                                              featureTypeName );
871                                    if ( concreteFeatureType == null ) {
872                                        msg = StringTools.concat( 200, "Lookup of concrete feature type '",
873                                                                  featureTypeName, "' failed: ",
874                                                                  " Inconsistent featuretype column!?" );
875                                        LOG.logError( msg );
876                                        throw new DatastoreException( msg );
877                                    }
878    
879                                    FeatureId fid = extractFeatureId( concreteFeatureType, rs, 2 );
880    
881                                    msg = StringTools.concat( 200, "Subfeature '", fid.getAsString(),
882                                                              "' has concrete feature type '",
883                                                              concreteFeatureType.getName(), "'." );
884                                    LOG.logDebug( msg );
885    
886                                    FeatureProperty property = null;
887                                    if ( !this.featuresInGeneration.contains( fid ) ) {
888                                        PropertyPath[] subPropertyPaths = PropertyPathResolver.determineSubPropertyPaths(
889                                                                                                                          concreteFeatureType,
890                                                                                                                          propertyPaths );
891                                        Feature feature = fetchFeature( fid, subPropertyPaths );
892                                        if ( feature != null ) {
893                                            property = FeatureFactory.createFeatureProperty( propertyName, feature );
894                                            result.add( property );
895                                        }
896    
897                                    } else {
898                                        property = FeatureFactory.createFeatureProperty( propertyName, null );
899                                        addToFidToPropertyMap( fid, property );
900                                        result.add( property );
901                                    }
902                                }
903                            }
904                        } else {
905                            msg = StringTools.concat( 200, "Querying of feature properties ",
906                                                      "with a content type with more than one ",
907                                                      "concrete substitution is not implemented for ",
908                                                      tableRelations.length, " TableRelations." );
909                            throw new DatastoreException( msg );
910                        }
911                    } else {
912                        // feature type is the only substitutable concrete feature type
913                        PropertyPath[] subPropertyPaths = PropertyPathResolver.determineSubPropertyPaths( ft, propertyPaths );
914                        // TODO aliases?
915                        Map<MappedPropertyType, Collection<PropertyPath>> requestedPropertiesMap = PropertyPathResolver.determineFetchProperties(
916                                                                                                                                                  ft,
917                                                                                                                                                  null,
918                                                                                                                                                  subPropertyPaths );
919                        MappedPropertyType[] requestedProperties = requestedPropertiesMap.keySet().toArray(
920                                                                                                            new MappedPropertyType[requestedPropertiesMap.size()] );
921    
922                        // determine contents (fields / functions) that needs to be SELECTed from
923                        // current table
924                        List<List<SimpleContent>> fetchContents = determineFetchContents( ft, requestedProperties );
925                        Map<SimpleContent, Integer> newResultPosMap = buildResultPosMap( fetchContents );
926    
927                        StatementBuffer query = buildSubsequentSelect( fetchContents, pt.getTableRelations(), resultValues,
928                                                                       resultPosMap );
929                        LOG.logDebug( "Subsequent query: '" + query + "'" );
930    
931                        if ( query != null ) {
932                            Object[] newResultValues = new Object[fetchContents.size()];
933                            stmt = this.datastore.prepareStatement( this.conn, query );
934                            rs = stmt.executeQuery();
935                            while ( rs.next() ) {
936                                // cache result values
937                                for ( int i = 0; i < newResultValues.length; i++ ) {
938                                    newResultValues[i] = rs.getObject( i + 1 );
939                                }
940                                FeatureId fid = extractFeatureId( ft, newResultPosMap, newResultValues );
941                                FeatureProperty property = null;
942                                if ( !this.featuresInGeneration.contains( fid ) ) {
943                                    Feature feature = extractFeature( fid, requestedPropertiesMap, newResultPosMap,
944                                                                      newResultValues );
945                                    property = FeatureFactory.createFeatureProperty( propertyName, feature );
946                                } else {
947                                    property = FeatureFactory.createFeatureProperty( propertyName, null );
948                                    addToFidToPropertyMap( fid, property );
949                                }
950                                result.add( property );
951                            }
952                        }
953                    }
954                } else {
955                    String msg = "Unsupported content type: '" + pt.getClass().getName()
956                                 + "' in QueryHandler.fetchRelatedProperties().";
957                    throw new IllegalArgumentException( msg );
958                }
959            } finally {
960                try {
961                    if ( rs != null ) {
962                        rs.close();
963                    }
964                    if ( stmt != null ) {
965                        stmt.close();
966                    }
967                } finally {
968                    if ( stmt != null ) {
969                        stmt.close();
970                    }
971                }
972            }
973            return result;
974        }
975    
976        private void addToFidToPropertyMap( FeatureId fid, FeatureProperty property ) {
977            List<FeatureProperty> properties = this.fidToPropertyMap.get( fid );
978            if ( properties == null ) {
979                properties = new ArrayList<FeatureProperty>();
980                this.fidToPropertyMap.put( fid, properties );
981            }
982            properties.add( property );
983        }
984    
985        protected void appendQualifiedContentList( StatementBuffer query, String tableAlias,
986                                                   List<List<SimpleContent>> fetchContents ) {
987    
988            for ( int i = 0; i < fetchContents.size(); i++ ) {
989                SimpleContent content = fetchContents.get( i ).get( 0 );
990                if ( content instanceof MappingField ) {
991                    appendQualifiedColumn( query, tableAlias, ( (MappingField) content ).getField() );
992                } else if ( content instanceof SQLFunctionCall ) {
993                    this.vcProvider.appendSQLFunctionCall( query, tableAlias, (SQLFunctionCall) content );
994                } else {
995                    assert false;
996                }
997                if ( i != fetchContents.size() - 1 ) {
998                    query.append( "," );
999                }
1000            }
1001        }
1002    
1003        /**
1004         * Builds a lookup map that allows to find the index (position in the {@link ResultSet}) by the
1005         * {@link SimpleContent} instance that makes it necessary to fetch it.
1006         * 
1007         * @param fetchContents
1008         * @return key: SimpleContent, value: Integer (position in ResultSet)
1009         */
1010        protected Map<SimpleContent, Integer> buildResultPosMap( List<List<SimpleContent>> fetchContents ) {
1011    
1012            Map<SimpleContent, Integer> resultPosMap = new HashMap<SimpleContent, Integer>();
1013            for ( int i = 0; i < fetchContents.size(); i++ ) {
1014                for ( SimpleContent content : fetchContents.get( i ) ) {
1015                    resultPosMap.put( content, i );
1016                }
1017            }
1018            return resultPosMap;
1019        }
1020    }