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