001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_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.PropertyPathResolver.determineFetchProperties;
039    import static org.deegree.model.feature.FeatureFactory.createFeatureProperty;
040    
041    import java.net.MalformedURLException;
042    import java.net.URL;
043    import java.sql.Connection;
044    import java.sql.PreparedStatement;
045    import java.sql.ResultSet;
046    import java.sql.SQLException;
047    import java.util.ArrayList;
048    import java.util.Collection;
049    import java.util.Collections;
050    import java.util.HashMap;
051    import java.util.HashSet;
052    import java.util.List;
053    import java.util.Map;
054    import java.util.Set;
055    
056    import org.deegree.datatypes.QualifiedName;
057    import org.deegree.framework.log.ILogger;
058    import org.deegree.framework.log.LoggerFactory;
059    import org.deegree.framework.util.StringTools;
060    import org.deegree.i18n.Messages;
061    import org.deegree.io.datastore.DatastoreException;
062    import org.deegree.io.datastore.FeatureId;
063    import org.deegree.io.datastore.PropertyPathResolver;
064    import org.deegree.io.datastore.schema.MappedFeaturePropertyType;
065    import org.deegree.io.datastore.schema.MappedFeatureType;
066    import org.deegree.io.datastore.schema.MappedGMLId;
067    import org.deegree.io.datastore.schema.MappedGeometryPropertyType;
068    import org.deegree.io.datastore.schema.MappedPropertyType;
069    import org.deegree.io.datastore.schema.MappedSimplePropertyType;
070    import org.deegree.io.datastore.schema.TableRelation;
071    import org.deegree.io.datastore.schema.content.ConstantContent;
072    import org.deegree.io.datastore.schema.content.MappingField;
073    import org.deegree.io.datastore.schema.content.MappingGeometryField;
074    import org.deegree.io.datastore.schema.content.SQLFunctionCall;
075    import org.deegree.io.datastore.schema.content.SimpleContent;
076    import org.deegree.model.crs.CRSFactory;
077    import org.deegree.model.crs.CoordinateSystem;
078    import org.deegree.model.crs.UnknownCRSException;
079    import org.deegree.model.feature.Feature;
080    import org.deegree.model.feature.FeatureFactory;
081    import org.deegree.model.feature.FeatureProperty;
082    import org.deegree.model.feature.schema.PropertyType;
083    import org.deegree.model.spatialschema.Geometry;
084    import org.deegree.ogcbase.PropertyPath;
085    import org.deegree.ogcwebservices.wfs.operation.Query;
086    
087    /**
088     * The only implementation of this abstract class is the {@link QueryHandler} class.
089     * <p>
090     * While the {@link QueryHandler} class performs the initial SELECT, {@link FeatureFetcher} is responsible for any
091     * subsequent SELECTs that may be necessary.
092     *
093     * @see QueryHandler
094     *
095     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
096     * @author last edited by: $Author: mschneider $
097     *
098     * @version $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18. Jun 2009) $
099     */
100    abstract class FeatureFetcher extends AbstractRequestHandler {
101    
102        private static final ILogger LOG = LoggerFactory.getLogger( FeatureFetcher.class );
103    
104        // key: feature id of features that are generated or are in generation
105        protected Set<FeatureId> featuresInGeneration = new HashSet<FeatureId>();
106    
107        // key: feature id value, value: Feature
108        protected Map<FeatureId, Feature> featureMap = new HashMap<FeatureId, Feature>( 1000 );
109    
110        // key: feature id value, value: property instances that contain the feature
111        protected Map<FeatureId, List<FeatureProperty>> fidToPropertyMap = new HashMap<FeatureId, List<FeatureProperty>>();
112    
113        // provides virtual content (constants, sql functions, ...)
114        protected VirtualContentProvider vcProvider;
115    
116        protected Query query;
117    
118        private CoordinateSystem queryCS;
119    
120        // key: geometry field, value: function call that transforms it to the queried CS
121        private Map<MappingGeometryField, SQLFunctionCall> fieldToTransformCall = new HashMap<MappingGeometryField, SQLFunctionCall>();
122    
123        FeatureFetcher( AbstractSQLDatastore datastore, TableAliasGenerator aliasGenerator, Connection conn, Query query )
124                                throws DatastoreException {
125            super( datastore, aliasGenerator, conn );
126            this.query = query;
127            if ( this.query.getSrsName() != null ) {
128                try {
129                    this.queryCS = CRSFactory.create( this.query.getSrsName() );
130                } catch ( UnknownCRSException e ) {
131                    throw new DatastoreException( e.getMessage(), e );
132                }
133            }
134        }
135    
136        /**
137         * Builds a SELECT statement to fetch features / properties that are stored in a related table.
138         *
139         * @param fetchContents
140         *            table columns / functions to fetch
141         * @param relations
142         *            table relations that lead to the table where the property is stored
143         * @param resultValues
144         *            all retrieved columns from one result set row
145         * @param resultPosMap
146         *            key class: SimpleContent, value class: Integer (this is the associated index in resultValues)
147         * @return the statement or null if the keys in resultValues contain NULL values
148         */
149        private StatementBuffer buildSubsequentSelect( List<List<SimpleContent>> fetchContents, TableRelation[] relations,
150                                                       Object[] resultValues, Map<SimpleContent, Integer> resultPosMap ) {
151    
152            this.aliasGenerator.reset();
153            String[] tableAliases = this.aliasGenerator.generateUniqueAliases( relations.length );
154    
155            StatementBuffer query = new StatementBuffer();
156            query.append( "SELECT " );
157            appendQualifiedContentList( query, tableAliases[tableAliases.length - 1], fetchContents );
158            query.append( " FROM " );
159            query.append( relations[0].getToTable() );
160            query.append( " " );
161            query.append( tableAliases[0] );
162    
163            // append joins
164            for ( int i = 1; i < relations.length; i++ ) {
165                query.append( " JOIN " );
166                query.append( relations[i].getToTable() );
167                query.append( " " );
168                query.append( tableAliases[i] );
169                query.append( " ON " );
170                MappingField[] fromFields = relations[i].getFromFields();
171                MappingField[] toFields = relations[i].getToFields();
172                for ( int j = 0; j < fromFields.length; j++ ) {
173                    query.append( tableAliases[i - 1] );
174                    query.append( '.' );
175                    query.append( fromFields[j].getField() );
176                    query.append( '=' );
177                    query.append( tableAliases[i] );
178                    query.append( '.' );
179                    query.append( toFields[j].getField() );
180                }
181            }
182    
183            // append key constraints
184            query.append( " WHERE " );
185            MappingField[] fromFields = relations[0].getFromFields();
186            MappingField[] toFields = relations[0].getToFields();
187            for ( int i = 0; i < fromFields.length; i++ ) {
188                int resultPos = resultPosMap.get( fromFields[i] );
189                Object keyValue = resultValues[resultPos];
190                if ( keyValue == null ) {
191                    return null;
192                }
193                query.append( tableAliases[0] );
194                query.append( '.' );
195                query.append( toFields[i].getField() );
196                query.append( "=?" );
197                query.addArgument( keyValue, toFields[i].getType() );
198                if ( i != fromFields.length - 1 ) {
199                    query.append( " AND " );
200                }
201            }
202            return query;
203        }
204    
205        /**
206         * Builds a SELECT statement to fetch the feature ids and the (concrete) feature types of feature properties that
207         * are stored in a related table (currently limited to *one* join table).
208         * <p>
209         * This is only necessary for feature properties that contain feature types with more than one possible
210         * substitution.
211         *
212         * @param relation1
213         *            first table relation that leads to the join table
214         * @param relation2
215         *            second table relation that leads to the table where the property is stored
216         * @param resultValues
217         *            all retrieved columns from one result set row
218         * @param mappingFieldMap
219         *            key class: MappingField, value class: Integer (this is the associated index in resultValues)
220         * @return the statement or null if the keys in resultValues contain NULL values
221         */
222        private StatementBuffer buildFeatureTypeSelect( TableRelation relation1, TableRelation relation2,
223                                                        Object[] resultValues, Map<?, ?> mappingFieldMap ) {
224            StatementBuffer query = new StatementBuffer();
225            query.append( "SELECT " );
226            // append feature type column
227            query.append( FT_COLUMN );
228            // append feature id columns
229            MappingField[] fidFields = relation2.getFromFields();
230            for ( int i = 0; i < fidFields.length; i++ ) {
231                query.append( ',' );
232                query.append( fidFields[i].getField() );
233            }
234            query.append( " FROM " );
235            query.append( relation1.getToTable() );
236            query.append( " WHERE " );
237            // append key constraints
238            MappingField[] fromFields = relation1.getFromFields();
239            MappingField[] toFields = relation1.getToFields();
240            for ( int i = 0; i < fromFields.length; i++ ) {
241                Integer resultPos = (Integer) mappingFieldMap.get( fromFields[i] );
242                Object keyValue = resultValues[resultPos.intValue()];
243                if ( keyValue == null ) {
244                    return null;
245                }
246                query.append( toFields[i].getField() );
247                query.append( "=?" );
248                query.addArgument( keyValue, toFields[i].getType() );
249                if ( i != fromFields.length - 1 ) {
250                    query.append( " AND " );
251                }
252            }
253            return query;
254        }
255    
256        /**
257         * Builds a SELECT statement to fetch the feature id and the (concrete) feature type of a feature property that is
258         * stored in a related table (with the fk in the current table).
259         * <p>
260         * This is only necessary for feature properties that contain feature types with more than one possible
261         * substitution.
262         *
263         * TODO: Select the FT_ column beforehand.
264         *
265         * @param relation
266         *            table relation that leads to the subfeature table
267         * @param resultValues
268         *            all retrieved columns from one result set row
269         * @param mappingFieldMap
270         *            key class: MappingField, value class: Integer (this is the associated index in resultValues)
271         * @return the statement or null if the keys in resultValues contain NULL values
272         */
273        private StatementBuffer buildFeatureTypeSelect( TableRelation relation, Object[] resultValues,
274                                                        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    
556                if ( pt instanceof MappedFeaturePropertyType && ( (MappedFeaturePropertyType) pt ).externalLinksAllowed() ) {
557                    MappingField fld = pt.getTableRelations()[0].getFromFields()[0];
558                    MappingField newFld = new MappingField( fld.getTable(), fld.getField() + "_external", fld.getType() );
559                    columnsMap.put( fld.getField() + "_external", Collections.<SimpleContent> singletonList( newFld ) );
560                }
561    
562                if ( tableRelations != null && tableRelations.length != 0 ) {
563                    // if property is not stored in feature type's table, retrieve key fields of
564                    // the first relation's 'From' element
565                    MappingField[] fields = tableRelations[0].getFromFields();
566                    for ( int k = 0; k < fields.length; k++ ) {
567                        List<SimpleContent> list = columnsMap.get( fields[k].getField() );
568                        if ( list == null ) {
569                            list = new ArrayList<SimpleContent>();
570                        }
571                        list.add( fields[k] );
572                        columnsMap.put( fields[k].getField(), list );
573                    }
574                    // if (content instanceof FeaturePropertyContent) {
575                    // if (tableRelations.length == 1) {
576                    // // if feature property contains an abstract feature type, retrieve
577                    // // feature type as well (stored in column named "FT_fk")
578                    // MappedFeatureType subFeatureType = ( (FeaturePropertyContent) content )
579                    // .getFeatureTypeReference().getFeatureType();
580                    // if (subFeatureType.isAbstract()) {
581                    // String typeColumn = FT_PREFIX + fields [0].getField();
582                    // columnsMap.put (typeColumn, new ArrayList ());
583                    // }
584                    // }
585                    // }
586                } else {
587                    String column = null;
588                    SimpleContent content = null;
589                    if ( pt instanceof MappedSimplePropertyType ) {
590                        content = ( (MappedSimplePropertyType) pt ).getContent();
591                        if ( content instanceof MappingField ) {
592                            column = ( (MappingField) content ).getField();
593                        } else {
594                            // ignore virtual properties here (handled below)
595                            continue;
596                        }
597                    } else if ( pt instanceof MappedGeometryPropertyType ) {
598                        content = determineFetchContent( (MappedGeometryPropertyType) pt );
599                        column = ( (MappedGeometryPropertyType) pt ).getMappingField().getField();
600                    } else {
601                        assert false;
602                    }
603                    List<SimpleContent> contentList = columnsMap.get( column );
604                    if ( contentList == null ) {
605                        contentList = new ArrayList<SimpleContent>();
606                    }
607                    contentList.add( content );
608                    columnsMap.put( column, contentList );
609                }
610            }
611    
612            fetchList.addAll( columnsMap.values() );
613    
614            // add functions that are necessary to build the requested feature properties
615            for ( int i = 0; i < requestedProps.length; i++ ) {
616                MappedPropertyType pt = (MappedPropertyType) requestedProps[i];
617                TableRelation[] tableRelations = pt.getTableRelations();
618                if ( tableRelations == null || tableRelations.length == 0 ) {
619                    if ( pt instanceof MappedSimplePropertyType ) {
620                        SimpleContent content = ( (MappedSimplePropertyType) pt ).getContent();
621                        if ( content instanceof SQLFunctionCall ) {
622                            List<SimpleContent> functionCallList = new ArrayList<SimpleContent>( 1 );
623                            functionCallList.add( content );
624                            fetchList.add( functionCallList );
625                        } else {
626                            // ignore other content types here
627                            continue;
628                        }
629                    }
630                }
631            }
632            return fetchList;
633        }
634    
635        /**
636         * Determines a {@link SimpleContent} object that represents the queried GeometryProperty in the requested SRS.
637         * <p>
638         * <ul>
639         * <li>If the query SRS is identical to the geometry field's SRS (and thus the SRS of the stored geometry, the
640         * corresponding {@link MappingGeometryField} is returned.</li>
641         * <li>If the query SRS differs from the geometry field's SRS (and thus the SRS of the stored geometry, an
642         * {@link SQLFunctionCall} is returned that refers to the stored geometry, but transforms it to the queried SRS.</li>
643         * </ul>
644         *
645         * @param pt
646         *            geometry property
647         * @return a <code>SimpleContent</code> instance that represents the queried geometry property
648         * @throws DatastoreException
649         *             if the transform call cannot be generated
650         */
651        private SimpleContent determineFetchContent( MappedGeometryPropertyType pt )
652                                throws DatastoreException {
653    
654            MappingGeometryField field = pt.getMappingField();
655            SimpleContent content = field;
656    
657            String queriedSRS = this.datastore.checkTransformation( pt, this.query.getSrsName() );
658            if ( queriedSRS != null ) {
659                content = this.fieldToTransformCall.get( field );
660                if ( content == null ) {
661                    try {
662                        queriedSRS = CRSFactory.create( queriedSRS ).getCRS().getIdentifier();
663                    } catch ( UnknownCRSException e ) {
664                        // this should not be possible anyway
665                        throw new DatastoreException( e );
666                    }
667                    content = this.datastore.buildSRSTransformCall( pt, queriedSRS );
668                    this.fieldToTransformCall.put( field, (SQLFunctionCall) content );
669                }
670            }
671            return content;
672        }
673    
674        /**
675         * Retrieves the feature with the given feature id.
676         *
677         * @param fid
678         * @param requestedPaths
679         * @return the feature with the given type and feature id, may be null
680         * @throws SQLException
681         * @throws DatastoreException
682         * @throws UnknownCRSException
683         */
684        private Feature fetchFeature( FeatureId fid, PropertyPath[] requestedPaths )
685                                throws SQLException, DatastoreException, UnknownCRSException {
686    
687            Feature feature = null;
688            MappedFeatureType ft = fid.getFeatureType();
689            // TODO what about aliases here?
690            Map<MappedPropertyType, Collection<PropertyPath>> requestedPropMap = determineFetchProperties( ft, null,
691                                                                                                           requestedPaths );
692            MappedPropertyType[] requestedProps = requestedPropMap.keySet().toArray(
693                                                                                     new MappedPropertyType[requestedPropMap.size()] );
694    
695            if ( requestedProps.length > 0 ) {
696    
697                // determine contents (fields / functions) that must be SELECTed from root table
698                List<List<SimpleContent>> fetchContents = determineFetchContents( ft, requestedProps );
699                Map<SimpleContent, Integer> resultPosMap = buildResultPosMap( fetchContents );
700    
701                // build feature query
702                StatementBuffer query = buildFeatureSelect( fid, ft.getTable(), fetchContents );
703                LOG.logDebug( "Feature query: '" + query + "'" );
704                Object[] resultValues = new Object[fetchContents.size()];
705                PreparedStatement stmt = null;
706                ResultSet rs = null;
707                try {
708                    stmt = this.datastore.prepareStatement( this.conn, query );
709                    rs = stmt.executeQuery();
710    
711                    if ( rs.next() ) {
712                        // collect result values
713                        for ( int i = 0; i < resultValues.length; i++ ) {
714                            resultValues[i] = rs.getObject( i + 1 );
715                        }
716                        feature = extractFeature( fid, requestedPropMap, resultPosMap, resultValues );
717                    } else {
718                        String msg = Messages.getMessage( "DATASTORE_FEATURE_QUERY_NO_RESULT", query.getQueryString() );
719                        LOG.logError( msg );
720                        throw new DatastoreException( msg );
721                    }
722                    if ( rs.next() ) {
723                        String msg = Messages.getMessage( "DATASTORE_FEATURE_QUERY_MORE_THAN_ONE_RESULT",
724                                                          query.getQueryString() );
725                        LOG.logError( msg );
726                        throw new DatastoreException( msg );
727                    }
728                } finally {
729                    try {
730                        if ( rs != null ) {
731                            rs.close();
732                        }
733                    } finally {
734                        if ( stmt != null ) {
735                            stmt.close();
736                        }
737                    }
738                }
739            }
740            return feature;
741        }
742    
743        /**
744         *
745         * @param propertyName
746         * @param pt
747         * @param propertyPaths
748         *            property paths that refer to the property to be extracted
749         * @param resultPosMap
750         *            key class: MappingField, value class: Integer (this is the associated index in resultValues)
751         * @param resultValues
752         *            all retrieved columns from one result set row
753         * @return Collection of FeatureProperty instances
754         * @throws SQLException
755         *             if a JDBC related error occurs
756         * @throws DatastoreException
757         * @throws UnknownCRSException
758         */
759        private Collection<FeatureProperty> fetchRelatedProperties( QualifiedName propertyName, MappedPropertyType pt,
760                                                                    Collection<PropertyPath> propertyPaths,
761                                                                    Map<SimpleContent, Integer> resultPosMap,
762                                                                    Object[] resultValues )
763                                throws SQLException, DatastoreException, UnknownCRSException {
764    
765            Collection<FeatureProperty> result = new ArrayList<FeatureProperty>( 100 );
766            PreparedStatement stmt = null;
767            ResultSet rs = null;
768            try {
769                if ( pt instanceof MappedSimplePropertyType ) {
770                    SimpleContent content = ( (MappedSimplePropertyType) pt ).getContent();
771    
772                    // TODO check for invalid content types
773                    List<SimpleContent> fetchContents = new ArrayList<SimpleContent>( 1 );
774                    List<List<SimpleContent>> fetchContentsList = new ArrayList<List<SimpleContent>>( 1 );
775                    fetchContents.add( content );
776                    fetchContentsList.add( fetchContents );
777    
778                    StatementBuffer query = buildSubsequentSelect( fetchContentsList, pt.getTableRelations(), resultValues,
779                                                                   resultPosMap );
780                    LOG.logDebug( "Subsequent query: '" + query + "'" );
781                    if ( query != null ) {
782                        stmt = this.datastore.prepareStatement( this.conn, query );
783                        rs = stmt.executeQuery();
784                        while ( rs.next() ) {
785                            Object propertyValue = datastore.convertFromDBType( rs.getObject( 1 ), pt.getType() );
786                            FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName, propertyValue );
787                            result.add( property );
788                        }
789                    }
790                } else if ( pt instanceof MappedGeometryPropertyType ) {
791                    SimpleContent content = ( (MappedGeometryPropertyType) pt ).getMappingField();
792                    CoordinateSystem cs = ( (MappedGeometryPropertyType) pt ).getCS();
793    
794                    if ( this.queryCS != null ) {
795                        cs = this.queryCS;
796                    }
797                    content = determineFetchContent( (MappedGeometryPropertyType) pt );
798    
799                    List<SimpleContent> fetchContents = new ArrayList<SimpleContent>( 1 );
800                    List<List<SimpleContent>> fetchContentsList = new ArrayList<List<SimpleContent>>( 1 );
801                    fetchContents.add( content );
802                    fetchContentsList.add( fetchContents );
803    
804                    StatementBuffer query = buildSubsequentSelect( fetchContentsList, pt.getTableRelations(), resultValues,
805                                                                   resultPosMap );
806                    LOG.logDebug( "Subsequent query: '" + query + "'" );
807                    if ( query != null ) {
808                        stmt = this.datastore.prepareStatement( this.conn, query );
809                        rs = stmt.executeQuery();
810                        while ( rs.next() ) {
811                            Object value = rs.getObject( 1 );
812                            Geometry geometry = this.datastore.convertDBToDeegreeGeometry( value, cs, this.conn );
813                            FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName, geometry );
814                            result.add( property );
815                        }
816                    }
817                } else if ( pt instanceof MappedFeaturePropertyType ) {
818                    MappedFeatureType ft = ( (MappedFeaturePropertyType) pt ).getFeatureTypeReference().getFeatureType();
819    
820                    if ( ( (MappedFeaturePropertyType) pt ).externalLinksAllowed() ) {
821                        MappingField fld = pt.getTableRelations()[0].getFromFields()[0];
822                        String ref = fld.getField() + "_external";
823                        MappingField key = new MappingField( fld.getTable(), ref, fld.getType() );
824                        for ( SimpleContent f : resultPosMap.keySet() ) {
825                            if ( f.equals( key ) ) {
826                                Object url = resultValues[resultPosMap.get( f )];
827                                if ( url != null ) {
828                                    URL u = new URL( (String) url );
829                                    result.add( createFeatureProperty( propertyName, u ) );
830                                }
831                            }
832                        }
833    
834                    }
835    
836                    MappedFeatureType[] substitutions = ft.getConcreteSubstitutions();
837                    if ( substitutions.length > 1 ) {
838                        // if feature type has more than one concrete substitution, determine concrete
839                        // feature type first
840                        String msg = StringTools.concat( 200, "FeatureType '", ft.getName(),
841                                                         "' has more than one concrete ",
842                                                         "substitution. Need to determine feature type table first." );
843                        LOG.logDebug( msg );
844                        LOG.logDebug( "Property: " + pt.getName() );
845                        TableRelation[] tableRelations = pt.getTableRelations();
846                        if ( tableRelations.length == 2 ) {
847                            StatementBuffer query = buildFeatureTypeSelect( tableRelations[0], tableRelations[1],
848                                                                            resultValues, resultPosMap );
849                            LOG.logDebug( "Feature type (and id) query: '" + query + "'" );
850                            if ( query != null ) {
851                                stmt = this.datastore.prepareStatement( this.conn, query );
852                                rs = stmt.executeQuery();
853                                while ( rs.next() ) {
854                                    String featureTypeName = rs.getString( 1 );
855                                    MappedFeatureType concreteFeatureType = ft.getGMLSchema().getFeatureType(
856                                                                                                              featureTypeName );
857                                    if ( concreteFeatureType == null ) {
858                                        msg = StringTools.concat( 200, "Lookup of concrete feature type '",
859                                                                  featureTypeName, "' failed: ",
860                                                                  " Inconsistent featuretype column!?" );
861                                        LOG.logError( msg );
862                                        throw new DatastoreException( msg );
863                                    }
864                                    FeatureId fid = extractFeatureId( concreteFeatureType, rs, 2 );
865                                    msg = StringTools.concat( 200, "Subfeature '", fid.getAsString(),
866                                                              "' has concrete feature type '",
867                                                              concreteFeatureType.getName(), "'." );
868                                    LOG.logDebug( msg );
869    
870                                    if ( !this.featuresInGeneration.contains( fid ) ) {
871                                        PropertyPath[] subPropertyPaths = PropertyPathResolver.determineSubPropertyPaths(
872                                                                                                                          concreteFeatureType,
873                                                                                                                          propertyPaths );
874                                        Feature feature = fetchFeature( fid, subPropertyPaths );
875                                        if ( feature != null ) {
876                                            FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName,
877                                                                                                             feature );
878                                            result.add( property );
879                                        }
880                                    } else {
881                                        FeatureProperty property = FeatureFactory.createFeatureProperty( propertyName, null );
882                                        addToFidToPropertyMap( fid, property );
883                                        result.add( property );
884                                    }
885                                }
886                            }
887                        } else if ( tableRelations.length == 1 ) {
888                            StatementBuffer query = buildFeatureTypeSelect( tableRelations[0], resultValues, resultPosMap );
889                            LOG.logDebug( "Feature type (and id) query: '" + query + "'" );
890                            if ( query != null ) {
891                                stmt = this.datastore.prepareStatement( this.conn, query );
892                                rs = stmt.executeQuery();
893                                while ( rs.next() ) {
894                                    String featureTypeName = rs.getString( 1 );
895                                    MappedFeatureType concreteFeatureType = ft.getGMLSchema().getFeatureType(
896                                                                                                              featureTypeName );
897                                    if ( concreteFeatureType == null ) {
898                                        msg = StringTools.concat( 200, "Lookup of concrete feature type '",
899                                                                  featureTypeName, "' failed: ",
900                                                                  " Inconsistent featuretype column!?" );
901                                        LOG.logError( msg );
902                                        throw new DatastoreException( msg );
903                                    }
904    
905                                    FeatureId fid = extractFeatureId( concreteFeatureType, rs, 2 );
906    
907                                    msg = StringTools.concat( 200, "Subfeature '", fid.getAsString(),
908                                                              "' has concrete feature type '",
909                                                              concreteFeatureType.getName(), "'." );
910                                    LOG.logDebug( msg );
911    
912                                    FeatureProperty property = null;
913                                    if ( !this.featuresInGeneration.contains( fid ) ) {
914                                        PropertyPath[] subPropertyPaths = PropertyPathResolver.determineSubPropertyPaths(
915                                                                                                                          concreteFeatureType,
916                                                                                                                          propertyPaths );
917                                        Feature feature = fetchFeature( fid, subPropertyPaths );
918                                        if ( feature != null ) {
919                                            property = FeatureFactory.createFeatureProperty( propertyName, feature );
920                                            result.add( property );
921                                        }
922    
923                                    } else {
924                                        property = FeatureFactory.createFeatureProperty( propertyName, null );
925                                        addToFidToPropertyMap( fid, property );
926                                        result.add( property );
927                                    }
928                                }
929                            }
930                        } else {
931                            msg = StringTools.concat( 200, "Querying of feature properties ",
932                                                      "with a content type with more than one ",
933                                                      "concrete substitution is not implemented for ",
934                                                      tableRelations.length, " TableRelations." );
935                            throw new DatastoreException( msg );
936                        }
937                    } else {
938                        // feature type is the only substitutable concrete feature type
939                        PropertyPath[] subPropertyPaths = PropertyPathResolver.determineSubPropertyPaths( ft, propertyPaths );
940                        // TODO aliases?
941                        Map<MappedPropertyType, Collection<PropertyPath>> requestedPropertiesMap = PropertyPathResolver.determineFetchProperties(
942                                                                                                                                                  ft,
943                                                                                                                                                  null,
944                                                                                                                                                  subPropertyPaths );
945                        MappedPropertyType[] requestedProperties = requestedPropertiesMap.keySet().toArray(
946                                                                                                            new MappedPropertyType[requestedPropertiesMap.size()] );
947    
948                        // determine contents (fields / functions) that needs to be SELECTed from
949                        // current table
950                        List<List<SimpleContent>> fetchContents = determineFetchContents( ft, requestedProperties );
951                        Map<SimpleContent, Integer> newResultPosMap = buildResultPosMap( fetchContents );
952    
953                        StatementBuffer query = buildSubsequentSelect( fetchContents, pt.getTableRelations(), resultValues,
954                                                                       resultPosMap );
955                        LOG.logDebug( "Subsequent query: '" + query + "'" );
956    
957                        if ( query != null ) {
958                            Object[] newResultValues = new Object[fetchContents.size()];
959                            stmt = this.datastore.prepareStatement( this.conn, query );
960                            rs = stmt.executeQuery();
961                            while ( rs.next() ) {
962                                // cache result values
963                                for ( int i = 0; i < newResultValues.length; i++ ) {
964                                    newResultValues[i] = rs.getObject( i + 1 );
965                                }
966                                FeatureId fid = extractFeatureId( ft, newResultPosMap, newResultValues );
967                                FeatureProperty property = null;
968                                if ( !this.featuresInGeneration.contains( fid ) ) {
969                                    Feature feature = extractFeature( fid, requestedPropertiesMap, newResultPosMap,
970                                                                      newResultValues );
971                                    property = FeatureFactory.createFeatureProperty( propertyName, feature );
972                                } else {
973                                    property = FeatureFactory.createFeatureProperty( propertyName, null );
974                                    addToFidToPropertyMap( fid, property );
975                                }
976                                result.add( property );
977                            }
978                        }
979                    }
980                } else {
981                    String msg = "Unsupported content type: '" + pt.getClass().getName()
982                                 + "' in QueryHandler.fetchRelatedProperties().";
983                    throw new IllegalArgumentException( msg );
984                }
985            } catch ( MalformedURLException e ) {
986                LOG.logError( "Unknown error", e );
987            } finally {
988                try {
989                    if ( rs != null ) {
990                        rs.close();
991                    }
992                    if ( stmt != null ) {
993                        stmt.close();
994                    }
995                } finally {
996                    if ( stmt != null ) {
997                        stmt.close();
998                    }
999                }
1000            }
1001            return result;
1002        }
1003    
1004        private void addToFidToPropertyMap( FeatureId fid, FeatureProperty property ) {
1005            List<FeatureProperty> properties = this.fidToPropertyMap.get( fid );
1006            if ( properties == null ) {
1007                properties = new ArrayList<FeatureProperty>();
1008                this.fidToPropertyMap.put( fid, properties );
1009            }
1010            properties.add( property );
1011        }
1012    
1013        protected void appendQualifiedContentList( StatementBuffer query, String tableAlias,
1014                                                   List<List<SimpleContent>> fetchContents ) {
1015    
1016            for ( int i = 0; i < fetchContents.size(); i++ ) {
1017                SimpleContent content = fetchContents.get( i ).get( 0 );
1018                if ( content instanceof MappingField ) {
1019                    if ( content instanceof MappingGeometryField ) {
1020                        datastore.appendGeometryColumnGet( query, tableAlias, ( (MappingField) content ).getField() );
1021                    } else {
1022                        appendQualifiedColumn( query, tableAlias, ( (MappingField) content ).getField() );
1023                    }
1024                } else if ( content instanceof SQLFunctionCall ) {
1025                    this.vcProvider.appendSQLFunctionCall( query, tableAlias, (SQLFunctionCall) content );
1026                } else {
1027                    assert false;
1028                }
1029                if ( i != fetchContents.size() - 1 ) {
1030                    query.append( "," );
1031                }
1032            }
1033        }
1034    
1035        /**
1036         * Builds a lookup map that allows to find the index (position in the {@link ResultSet}) by the
1037         * {@link SimpleContent} instance that makes it necessary to fetch it.
1038         *
1039         * @param fetchContents
1040         * @return key: SimpleContent, value: Integer (position in ResultSet)
1041         */
1042        protected Map<SimpleContent, Integer> buildResultPosMap( List<List<SimpleContent>> fetchContents ) {
1043    
1044            Map<SimpleContent, Integer> resultPosMap = new HashMap<SimpleContent, Integer>();
1045            for ( int i = 0; i < fetchContents.size(); i++ ) {
1046                for ( SimpleContent content : fetchContents.get( i ) ) {
1047                    resultPosMap.put( content, i );
1048                }
1049            }
1050            return resultPosMap;
1051        }
1052    }