001    //$HeadURL: http://svn.wald.intevation.org/svn/deegree/base/trunk/src/org/deegree/io/datastore/sql/QueryHandler.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 java.sql.Connection;
039    import java.sql.PreparedStatement;
040    import java.sql.ResultSet;
041    import java.sql.SQLException;
042    import java.util.ArrayList;
043    import java.util.Collection;
044    import java.util.HashSet;
045    import java.util.List;
046    import java.util.Map;
047    import java.util.Set;
048    
049    import org.deegree.datatypes.QualifiedName;
050    import org.deegree.framework.log.ILogger;
051    import org.deegree.framework.log.LoggerFactory;
052    import org.deegree.io.datastore.DatastoreException;
053    import org.deegree.io.datastore.FeatureId;
054    import org.deegree.io.datastore.PropertyPathResolvingException;
055    import org.deegree.io.datastore.schema.MappedFeatureType;
056    import org.deegree.io.datastore.schema.MappedPropertyType;
057    import org.deegree.io.datastore.schema.content.SimpleContent;
058    import org.deegree.io.datastore.sql.wherebuilder.QueryTableTree;
059    import org.deegree.io.datastore.sql.wherebuilder.WhereBuilder;
060    import org.deegree.model.crs.UnknownCRSException;
061    import org.deegree.model.feature.Feature;
062    import org.deegree.model.feature.FeatureCollection;
063    import org.deegree.model.feature.FeatureFactory;
064    import org.deegree.model.feature.FeatureProperty;
065    import org.deegree.model.feature.FeatureTupleCollection;
066    import org.deegree.model.filterencoding.PropertyName;
067    import org.deegree.model.spatialschema.Envelope;
068    import org.deegree.model.spatialschema.GeometryException;
069    import org.deegree.ogcbase.PropertyPath;
070    import org.deegree.ogcwebservices.wfs.operation.Query;
071    import org.deegree.ogcwebservices.wfs.operation.GetFeature.RESULT_TYPE;
072    
073    /**
074     * Handles {@link Query} requests to SQL backed datastores.
075     * 
076     * @see FeatureFetcher
077     * @see AbstractSQLDatastore
078     * @see QueryTableTree
079     * 
080     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a>
081     * @author last edited by: $Author: apoth $
082     * 
083     * @version $Revision: 29956 $, $Date: 2011-03-09 14:51:48 +0100 (Wed, 09 Mar 2011) $
084     */
085    public class QueryHandler extends FeatureFetcher {
086    
087        private static final ILogger LOG = LoggerFactory.getLogger( QueryHandler.class );
088    
089        private Query query;
090    
091        // targeted feature types, more than one implies that a join of feature types is requested
092        private MappedFeatureType[] rootFts;
093    
094        // used to build the initial SELECT (especially the WHERE-clause)
095        private WhereBuilder whereBuilder;
096    
097        // TODO hack for making gml:boundedBy available even if the geometry properties are not queried
098        private List<PropertyName> augmentedGeoProps;
099    
100        /**
101         * Creates a new instance of <code>QueryHandler</code> from the given parameters.
102         * 
103         * @param ds
104         *            datastore that spawned this QueryHandler
105         * @param aliasGenerator
106         *            used to generate unique aliases for the tables in the SELECT statements
107         * @param conn
108         *            JDBCConnection to execute the generated SELECT statements against
109         * @param rootFts
110         *            the root feature types that are queried, more than one type means that the types are joined
111         * @param query
112         *            query to perform
113         * @throws DatastoreException
114         */
115        public QueryHandler( AbstractSQLDatastore ds, TableAliasGenerator aliasGenerator, Connection conn,
116                             MappedFeatureType[] rootFts, Query query ) throws DatastoreException {
117    
118            super( ds, aliasGenerator, conn, query );
119    
120            this.query = query;
121            this.rootFts = rootFts;
122            this.vcProvider = new VirtualContentProvider( query.getFilter(), ds, conn );
123            this.whereBuilder = this.datastore.getWhereBuilder( rootFts, query.getAliases(), query.getFilter(),
124                                                                query.getSortProperties(), aliasGenerator, this.vcProvider );
125            this.aliasGenerator = aliasGenerator;
126        }
127    
128        /**
129         * Performs the associated {@link Query} against the datastore.
130         * 
131         * @return collection of requested features
132         * @throws SQLException
133         *             if a JDBC error occurs
134         * @throws DatastoreException
135         * @throws UnknownCRSException
136         */
137        public FeatureCollection performQuery()
138                                throws SQLException, DatastoreException, UnknownCRSException {
139    
140            long start = -1;
141            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
142                start = System.currentTimeMillis();
143            }
144    
145            FeatureCollection result = null;
146    
147            if ( this.query.getResultType() == RESULT_TYPE.HITS ) {
148                result = performHitsQuery();
149            } else {
150                result = performResultsQuery();
151            }
152    
153            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
154                long elapsed = System.currentTimeMillis() - start;
155                LOG.logDebug( "Performing of query took " + elapsed + " milliseconds." );
156            }
157    
158            return result;
159        }
160    
161        /**
162         * Performs a query for the feature instances that match the filter constraints. This corresponds to a query with
163         * resultType=RESULTS.
164         * 
165         * @return collection of requested features
166         * @throws PropertyPathResolvingException
167         * @throws SQLException
168         * @throws DatastoreException
169         * @throws UnknownCRSException
170         */
171        private FeatureCollection performResultsQuery()
172                                throws PropertyPathResolvingException, SQLException, DatastoreException,
173                                UnknownCRSException {
174    
175            FeatureCollection result = null;
176    
177            SelectManager selectManager = new SelectManager( query, this.rootFts, this );
178            LOG.logDebug( "SelectManager: " + selectManager );
179    
180            // build initial SQL query
181            StatementBuffer querybuf = buildInitialSelect( selectManager );
182            LOG.logDebug( "Initial query: '" + querybuf + "'" );
183    
184            PreparedStatement stmt = null;
185            ResultSet rs = null;
186    
187            try {
188                stmt = this.datastore.prepareStatement( this.conn, querybuf );
189                rs = stmt.executeQuery();
190                if ( this.rootFts.length == 1 ) {
191                    result = performSimpleResultsQuery( rs, selectManager );
192                } else {
193                    result = performJoinResultsQuery( rs, selectManager );
194                }
195            } finally {
196                try {
197                    if ( rs != null ) {
198                        rs.close();
199                    }
200                } finally {
201                    if ( stmt != null ) {
202                        stmt.close();
203                    }
204                }
205            }
206    
207            resolveFeatureReferences();
208            result.setAttribute( "numberOfFeatures", "" + result.size() );
209            return result;
210        }
211    
212        private FeatureCollection performSimpleResultsQuery( ResultSet rs, SelectManager selectManager )
213                                throws DatastoreException, SQLException, UnknownCRSException {
214    
215            MappedFeatureType rootFt = this.rootFts[0];
216            Map<MappedPropertyType, Collection<PropertyPath>> requestedPropertyMap = selectManager.getAllFetchProps()[0];
217            Map<SimpleContent, Integer> resultPosMap = selectManager.getResultPosMaps()[0];
218    
219            FeatureCollection result = FeatureFactory.createFeatureCollection( "ID", 10000 );
220            Object[] resultValues = new Object[selectManager.getFetchContentCount()];
221    
222            // used to handle that a feature may occur several times in result set
223            Set<FeatureId> rootFeatureIds = new HashSet<FeatureId>();
224    
225            // skip features in resultSet (startPosition is first feature to be included)
226            int startPosition = this.query.getStartPosition();
227            Set<FeatureId> skippedFeatures = new HashSet<FeatureId>();
228            while ( skippedFeatures.size() < startPosition - 1 && rs.next() ) {
229                LOG.logDebug( "Skipping result row." );
230                // collect result values
231                for ( int i = 0; i < resultValues.length; i++ ) {
232                    resultValues[i] = rs.getObject( i + 1 );
233                }
234                FeatureId fid = extractFeatureId( rootFt, selectManager.getResultPosMaps()[0], resultValues );
235                skippedFeatures.add( fid );
236            }
237    
238            int maxFeatures = this.query.getMaxFeatures();
239            while ( rs.next() ) {
240    
241                // already maxFeature features extracted?
242                if ( maxFeatures != -1 && rootFeatureIds.size() == maxFeatures ) {
243                    break;
244                }
245    
246                // collect result values
247                for ( int i = 0; i < resultValues.length; i++ ) {
248                    resultValues[i] = rs.getObject( i + 1 );
249                }
250    
251                FeatureId fid = extractFeatureId( rootFt, resultPosMap, resultValues );
252    
253                // skip it if this root feature has already been fetched or if it is a feature
254                // (again) that has been skipped
255                if ( !rootFeatureIds.contains( fid ) && !skippedFeatures.contains( fid ) ) {
256    
257                    rootFeatureIds.add( fid );
258    
259                    // feature may have been fetched as a subfeature already
260                    Feature feature = this.featureMap.get( fid );
261                    if ( feature == null ) {
262                        feature = extractFeature( fid, requestedPropertyMap, resultPosMap, resultValues );
263                        // hack that ensures that the boundedBy information is correct
264                        if ( !selectManager.augmentedGeometryProps.isEmpty() ) {
265                            try {
266                                Envelope boundedBy = feature.getBoundedBy();
267                                for ( PropertyPath unqueriedGeoProp : selectManager.augmentedGeometryProps ) {
268                                    LOG.logDebug( "Removing " + unqueriedGeoProp + " from feature instance." );
269                                    feature.removeProperty( unqueriedGeoProp.getStep( 1 ).getPropertyName() );
270                                }
271                                feature.setProperty(
272                                                     FeatureFactory.createFeatureProperty(
273                                                                                           new QualifiedName( "boundedBy" ),
274                                                                                           boundedBy ), 1 );
275                            } catch ( GeometryException e ) {
276                                // TODO Auto-generated catch block
277                                e.printStackTrace();
278                            }
279                        }
280    
281                    }
282                    result.add( feature );
283                }
284            }
285            return result;
286        }
287    
288        private FeatureTupleCollection performJoinResultsQuery( ResultSet rs, SelectManager selectManager )
289                                throws DatastoreException, SQLException, UnknownCRSException {
290    
291            List<Feature[]> resultTuples = new ArrayList<Feature[]>();
292    
293            // used to handle that a feature may occur several times in result set
294            Set<String> rootFeatureIds = new HashSet<String>( 1000 );
295    
296            Object[] resultValues = new Object[selectManager.getFetchContentCount()];
297            int maxFeatures = this.query.getMaxFeatures();
298    
299            int[] resultFtIdx = selectManager.getIncludedFtIdx();
300            for ( int i = 0; i < resultFtIdx.length; i++ ) {
301                LOG.logDebug( "Included in result set: " + resultFtIdx[i] );
302            }
303    
304            while ( rs.next() ) {
305    
306                Feature[] resultTuple = new Feature[resultFtIdx.length];
307    
308                // already maxFeature features extracted?
309                if ( maxFeatures != -1 && resultTuples.size() == maxFeatures ) {
310                    break;
311                }
312    
313                // collect result values
314                for ( int i = 0; i < resultValues.length; i++ ) {
315                    resultValues[i] = rs.getObject( i + 1 );
316                }
317    
318                FeatureId[] fids = new FeatureId[resultFtIdx.length];
319                StringBuffer combinedFid = new StringBuffer();
320    
321                // build combined fid to identify unique "features" (actually these are feature tuples)
322                for ( int i = 0; i < resultFtIdx.length; i++ ) {
323                    int idx = resultFtIdx[i];
324                    MappedFeatureType rootFt = this.rootFts[idx];
325                    Map<SimpleContent, Integer> resultPosMap = selectManager.getResultPosMaps()[idx];
326                    fids[i] = extractFeatureId( rootFt, resultPosMap, resultValues );
327                    combinedFid.append( fids[i].getAsString() );
328                }
329                LOG.logDebug( "CombinedFID: " + combinedFid );
330    
331                // if tuple has not been added to result yet, extract and add it
332                if ( !rootFeatureIds.contains( combinedFid.toString() ) ) {
333                    for ( int i = 0; i < resultFtIdx.length; i++ ) {
334                        int ftIdx = resultFtIdx[i];
335                        FeatureId fid = fids[i];
336                        Map<MappedPropertyType, Collection<PropertyPath>> requestedPropertyMap = selectManager.getAllFetchProps()[ftIdx];
337                        Map<SimpleContent, Integer> resultPosMap = selectManager.getResultPosMaps()[ftIdx];
338    
339                        // feature may have been fetched already
340                        Feature feature = this.featureMap.get( fid );
341                        if ( feature == null ) {
342                            feature = extractFeature( fid, requestedPropertyMap, resultPosMap, resultValues );
343                        }
344                        resultTuple[i] = ( feature );
345                    }
346                    resultTuples.add( resultTuple );
347                    rootFeatureIds.add( combinedFid.toString() );
348                }
349            }
350    
351            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
352                for ( int i = 0; i < resultTuples.size(); i++ ) {
353                    Feature[] resultTuple = resultTuples.get( i );
354                    StringBuffer sb = new StringBuffer();
355                    for ( int j = 0; j < resultFtIdx.length; j++ ) {
356                        int idx = resultFtIdx[j];
357                        sb.append( resultTuple[j].getId() );
358                        if ( idx != this.rootFts.length - 1 ) {
359                            sb.append( ',' );
360                        }
361                    }
362                    LOG.logDebug( sb.toString() );
363                }
364            }
365    
366            FeatureTupleCollection result = FeatureFactory.createFeatureCollection( "id", resultTuples, this.rootFts.length );
367            return result;
368        }
369    
370        private void resolveFeatureReferences() {
371            for ( FeatureId fid : this.fidToPropertyMap.keySet() ) {
372                Feature feature = this.featureMap.get( fid );
373                assert feature != null;
374                for ( FeatureProperty property : this.fidToPropertyMap.get( fid ) ) {
375                    property.setValue( feature );
376                }
377            }
378        }
379    
380        /**
381         * Performs a query for the number feature instances that match the query constraints. This corresponds to a query
382         * with resultType=HITS.
383         * 
384         * @return a feature collection containing number of features that match the query constraints
385         * @throws SQLException
386         * @throws DatastoreException
387         */
388        private FeatureCollection performHitsQuery()
389                                throws SQLException, DatastoreException {
390    
391            FeatureCollection result = FeatureFactory.createFeatureCollection( "ID", 2 );
392    
393            String tableAlias = this.whereBuilder.getRootTableAlias( 0 );
394            String field = this.rootFts[0].getGMLId().getIdFields()[0].getField();
395            StatementBuffer query = new StatementBuffer();
396            query.append( "SELECT COUNT( DISTINCT " );
397            query.append( tableAlias + '.' + field );
398            query.append( ") FROM " );
399    
400            whereBuilder.appendJoinTableList( query );
401            whereBuilder.appendWhereCondition( query );
402            LOG.logDebug( "Count query: '" + query + "'" );
403    
404            ResultSet rs = null;
405            PreparedStatement stmt = this.datastore.prepareStatement( this.conn, query );
406            try {
407                rs = stmt.executeQuery();
408                if ( rs.next() ) {
409                    result.setAttribute( "numberOfFeatures", rs.getObject( 1 ).toString() );
410                } else {
411                    LOG.logError( "Internal error. Count result is empty (no rows)." );
412                    throw new SQLException();
413                }
414            } catch ( SQLException e ) {
415                LOG.logError( e.getMessage(), e );
416                throw new SQLException( "Error performing count (HITS) query: " + query );
417            } finally {
418                try {
419                    if ( rs != null ) {
420                        rs.close();
421                    }
422                } finally {
423                    if ( stmt != null ) {
424                        stmt.close();
425                    }
426                }
427            }
428            return result;
429        }
430    
431        /**
432         * Builds the initial SELECT statement.
433         * <p>
434         * This statement determines all feature ids that are affected by the filter, but also SELECTs all properties that
435         * are stored in the root feature types' tables (to improve efficiency).
436         * </p>
437         * <p>
438         * The statement is structured like this:
439         * <ul>
440         * <li><code>SELECT</code></li>
441         * <li>comma-separated list of qualified columns/functions to fetch from root tables</li>
442         * <li><code>FROM</code></li>
443         * <li>comma-separated list of tables and their aliases (this is needed to constrain the paths to selected
444         * XPath-PropertyNames)</li>
445         * <li><code>WHERE</code></li>
446         * <li>SQL representation of the filter expression</li>
447         * <li><code>ORDER BY</code></li>
448         * <li>qualified sort criteria columns/functions</li>
449         * </ul>
450         * </p>
451         * 
452         * @param selectManager
453         *            associated <code>SelectManager</code>
454         * @return initial select statement
455         * @throws DatastoreException
456         */
457        protected StatementBuffer buildInitialSelect( SelectManager selectManager )
458                                throws DatastoreException {
459    
460            List<List<SimpleContent>>[] fetchContents = selectManager.getAllFetchContents();
461            StatementBuffer stmt = new StatementBuffer();
462    
463            stmt.append( "SELECT " );
464    
465            String tableAlias = this.whereBuilder.getRootTableAlias( 0 );
466            List<List<SimpleContent>> ftFetchContents = fetchContents[0];
467            appendQualifiedContentList( stmt, tableAlias, ftFetchContents );
468    
469            boolean first = ftFetchContents.size() == 0;
470            for ( int i = 1; i < this.rootFts.length; i++ ) {
471                ftFetchContents = fetchContents[i];
472                if ( ftFetchContents.size() > 0 ) {
473                    if ( !first ) {
474                        stmt.append( ',' );
475                        first = false;
476                    }
477                    tableAlias = this.whereBuilder.getRootTableAlias( i );
478                    appendQualifiedContentList( stmt, tableAlias, ftFetchContents );
479                }
480            }
481    
482            stmt.append( " FROM " );
483    
484            whereBuilder.appendJoinTableList( stmt );
485            whereBuilder.appendWhereCondition( stmt );
486            whereBuilder.appendOrderByCondition( stmt );
487    
488            return stmt;
489        }
490    }