001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/io/datastore/sql/QueryHandler.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2008 by:
006     EXSE, Department of Geography, University of Bonn
007     http://www.giub.uni-bonn.de/deegree/
008     lat/lon GmbH
009     http://www.lat-lon.de
010    
011     This library is free software; you can redistribute it and/or
012     modify it under the terms of the GNU Lesser General Public
013     License as published by the Free Software Foundation; either
014     version 2.1 of the License, or (at your option) any later version.
015    
016     This library is distributed in the hope that it will be useful,
017     but WITHOUT ANY WARRANTY; without even the implied warranty of
018     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
019     Lesser General Public License for more details.
020    
021     You should have received a copy of the GNU Lesser General Public
022     License along with this library; if not, write to the Free Software
023     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
024    
025     Contact:
026    
027     Andreas Poth
028     lat/lon GmbH
029     Aennchenstraße 19
030     53177 Bonn
031     Germany
032     E-Mail: poth@lat-lon.de
033    
034     Prof. Dr. Klaus Greve
035     Department of Geography
036     University of Bonn
037     Meckenheimer Allee 166
038     53115 Bonn
039     Germany
040     E-Mail: greve@giub.uni-bonn.de
041    
042     ---------------------------------------------------------------------------*/
043    package org.deegree.io.datastore.sql;
044    
045    import java.sql.Connection;
046    import java.sql.PreparedStatement;
047    import java.sql.ResultSet;
048    import java.sql.SQLException;
049    import java.util.ArrayList;
050    import java.util.Collection;
051    import java.util.HashSet;
052    import java.util.List;
053    import java.util.Map;
054    import java.util.Set;
055    
056    import org.deegree.framework.log.ILogger;
057    import org.deegree.framework.log.LoggerFactory;
058    import org.deegree.io.datastore.DatastoreException;
059    import org.deegree.io.datastore.FeatureId;
060    import org.deegree.io.datastore.PropertyPathResolvingException;
061    import org.deegree.io.datastore.schema.MappedFeatureType;
062    import org.deegree.io.datastore.schema.MappedPropertyType;
063    import org.deegree.io.datastore.schema.content.SimpleContent;
064    import org.deegree.io.datastore.sql.wherebuilder.QueryTableTree;
065    import org.deegree.io.datastore.sql.wherebuilder.WhereBuilder;
066    import org.deegree.model.crs.UnknownCRSException;
067    import org.deegree.model.feature.Feature;
068    import org.deegree.model.feature.FeatureCollection;
069    import org.deegree.model.feature.FeatureFactory;
070    import org.deegree.model.feature.FeatureProperty;
071    import org.deegree.model.feature.FeatureTupleCollection;
072    import org.deegree.ogcbase.PropertyPath;
073    import org.deegree.ogcwebservices.wfs.operation.Query;
074    import org.deegree.ogcwebservices.wfs.operation.GetFeature.RESULT_TYPE;
075    
076    /**
077     * Handles {@link Query} requests to SQL backed datastores.
078     * 
079     * @see FeatureFetcher
080     * @see AbstractSQLDatastore
081     * @see QueryTableTree
082     * 
083     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a>
084     * @author last edited by: $Author: apoth $
085     * 
086     * @version $Revision: 9342 $, $Date: 2007-12-27 13:32:57 +0100 (Do, 27 Dez 2007) $
087     */
088    public class QueryHandler extends FeatureFetcher {
089    
090        private static final ILogger LOG = LoggerFactory.getLogger( QueryHandler.class );
091    
092        private Query query;
093    
094        // targeted feature types, more than one implies that a join of feature types is requested
095        private MappedFeatureType[] rootFts;
096    
097        // used to build the initial SELECT (especially the WHERE-clause)
098        private WhereBuilder whereBuilder;
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                    }
264                    result.add( feature );
265                }
266            }
267            return result;
268        }
269    
270        private FeatureTupleCollection performJoinResultsQuery( ResultSet rs, SelectManager selectManager )
271                                throws DatastoreException, SQLException, UnknownCRSException {
272    
273            List<Feature[]> resultTuples = new ArrayList<Feature[]>();
274    
275            // used to handle that a feature may occur several times in result set
276            Set<String> rootFeatureIds = new HashSet<String>( 1000 );
277    
278            Object[] resultValues = new Object[selectManager.getFetchContentCount()];
279            int maxFeatures = this.query.getMaxFeatures();
280    
281            int[] resultFtIdx = selectManager.getIncludedFtIdx();
282            for ( int i = 0; i < resultFtIdx.length; i++ ) {
283                LOG.logDebug ("Included in result set: " + resultFtIdx [i]);
284            }
285    
286            while ( rs.next() ) {
287    
288                Feature[] resultTuple = new Feature[resultFtIdx.length];
289    
290                // already maxFeature features extracted?
291                if ( maxFeatures != -1 && resultTuples.size() == maxFeatures ) {
292                    break;
293                }
294    
295                // collect result values
296                for ( int i = 0; i < resultValues.length; i++ ) {
297                    resultValues[i] = rs.getObject( i + 1 );
298                }
299    
300                FeatureId[] fids = new FeatureId[resultFtIdx.length];
301                StringBuffer combinedFid = new StringBuffer();
302    
303                // build combined fid to identify unique "features" (actually these are feature tuples)
304                for ( int i = 0; i < resultFtIdx.length; i++ ) {
305                    int idx = resultFtIdx[i];
306                    MappedFeatureType rootFt = this.rootFts[idx];
307                    Map<SimpleContent, Integer> resultPosMap = selectManager.getResultPosMaps()[idx];
308                    fids[i] = extractFeatureId( rootFt, resultPosMap, resultValues );
309                    combinedFid.append( fids[i].getAsString() );
310                }
311                LOG.logDebug ("CombinedFID: " + combinedFid);
312    
313                // if tuple has not been added to result yet, extract and add it
314                if ( !rootFeatureIds.contains( combinedFid.toString () ) ) {
315                    for ( int i = 0; i < resultFtIdx.length; i++ ) {
316                        int ftIdx = resultFtIdx[i];
317                        FeatureId fid = fids[i];
318                        Map<MappedPropertyType, Collection<PropertyPath>> requestedPropertyMap = selectManager.getAllFetchProps()[ftIdx];
319                        Map<SimpleContent, Integer> resultPosMap = selectManager.getResultPosMaps()[ftIdx];
320    
321                        // feature may have been fetched already
322                        Feature feature = this.featureMap.get( fid );
323                        if ( feature == null ) {
324                            feature = extractFeature( fid, requestedPropertyMap, resultPosMap, resultValues );
325                        }
326                        resultTuple[i] = ( feature );
327                    }
328                    resultTuples.add( resultTuple );
329                    rootFeatureIds.add( combinedFid.toString() );                
330                }
331            }
332    
333            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
334                for ( int i = 0; i < resultTuples.size(); i++ ) {
335                    Feature[] resultTuple = resultTuples.get( i );
336                    StringBuffer sb = new StringBuffer();
337                    for ( int j = 0; j < resultFtIdx.length; j++ ) {
338                        int idx = resultFtIdx[j];
339                        sb.append( resultTuple[j].getId() );
340                        if ( idx != this.rootFts.length - 1 ) {
341                            sb.append( ',' );
342                        }
343                    }
344                    LOG.logDebug( sb.toString() );
345                }
346            }
347    
348            FeatureTupleCollection result = FeatureFactory.createFeatureCollection( "id", resultTuples, this.rootFts.length );
349            return result;
350        }
351    
352        private void resolveFeatureReferences() {
353            for ( FeatureId fid : this.fidToPropertyMap.keySet() ) {
354                Feature feature = this.featureMap.get( fid );
355                assert feature != null;
356                for ( FeatureProperty property : this.fidToPropertyMap.get( fid ) ) {
357                    property.setValue( feature );
358                }
359            }
360        }
361    
362        /**
363         * Performs a query for the number feature instances that match the query constraints. This corresponds to a query
364         * with resultType=HITS.
365         * 
366         * @return a feature collection containing number of features that match the query constraints
367         * @throws SQLException
368         * @throws DatastoreException
369         */
370        private FeatureCollection performHitsQuery()
371                                throws SQLException, DatastoreException {
372    
373            FeatureCollection result = FeatureFactory.createFeatureCollection( "ID", 2 );
374    
375            String tableAlias = this.whereBuilder.getRootTableAlias( 0 );
376            String field = this.rootFts[0].getGMLId().getIdFields()[0].getField();
377            StatementBuffer query = new StatementBuffer();
378            query.append( "SELECT COUNT( DISTINCT " );
379            query.append( tableAlias + '.' + field );
380            query.append( ") FROM " );
381    
382            whereBuilder.appendJoinTableList( query );
383            whereBuilder.appendWhereCondition( query );
384            LOG.logDebug( "Count query: '" + query + "'" );
385    
386            ResultSet rs = null;
387            PreparedStatement stmt = this.datastore.prepareStatement( this.conn, query );
388            try {
389                rs = stmt.executeQuery();
390                if ( rs.next() ) {
391                    result.setAttribute( "numberOfFeatures", rs.getObject( 1 ).toString() );
392                } else {
393                    LOG.logError( "Internal error. Count result is empty (no rows)." );
394                    throw new SQLException();
395                }
396            } catch ( SQLException e ) {
397                LOG.logError( e.getMessage(), e );
398                throw new SQLException( "Error performing count (HITS) query: " + query );
399            } finally {
400                try {
401                    if ( rs != null ) {
402                        rs.close();
403                    }
404                } finally {
405                    if ( stmt != null ) {
406                        stmt.close();
407                    }
408                }
409            }
410            return result;
411        }
412    
413        /**
414         * Builds the initial SELECT statement.
415         * <p>
416         * This statement determines all feature ids that are affected by the filter, but also SELECTs all properties that
417         * are stored in the root feature types' tables (to improve efficiency).
418         * </p>
419         * <p>
420         * The statement is structured like this:
421         * <ul>
422         * <li><code>SELECT</code></li>
423         * <li>comma-separated list of qualified columns/functions to fetch from root tables</li>
424         * <li><code>FROM</code></li>
425         * <li>comma-separated list of tables and their aliases (this is needed to constrain the paths to selected
426         * XPath-PropertyNames)</li>
427         * <li><code>WHERE</code></li>
428         * <li>SQL representation of the filter expression</li>
429         * <li><code>ORDER BY</code></li>
430         * <li>qualified sort criteria columns/functions</li>
431         * </ul>
432         * </p>
433         * 
434         * @param selectManager
435         *            associated <code>SelectManager</code>
436         * @return initial select statement
437         * @throws DatastoreException
438         */
439        protected StatementBuffer buildInitialSelect( SelectManager selectManager )
440                                throws DatastoreException {
441    
442            List<List<SimpleContent>>[] fetchContents = selectManager.getAllFetchContents();
443            StatementBuffer stmt = new StatementBuffer();
444    
445            stmt.append( "SELECT " );
446    
447            String tableAlias = this.whereBuilder.getRootTableAlias( 0 );
448            List<List<SimpleContent>> ftFetchContents = fetchContents[0];
449            appendQualifiedContentList( stmt, tableAlias, ftFetchContents );
450    
451            boolean first = ftFetchContents.size() == 0;        
452            for ( int i = 1; i < this.rootFts.length; i++ ) {
453                ftFetchContents = fetchContents[i];            
454                if ( ftFetchContents.size() > 0 ) {
455                    if (!first) {
456                        stmt.append( ',' );
457                        first = false;
458                    }
459                    tableAlias = this.whereBuilder.getRootTableAlias( i );
460                    appendQualifiedContentList( stmt, tableAlias, ftFetchContents );
461                }
462            }
463    
464            stmt.append( " FROM " );
465    
466            whereBuilder.appendJoinTableList( stmt );
467            whereBuilder.appendWhereCondition( stmt );
468            whereBuilder.appendOrderByCondition( stmt );
469    
470            return stmt;
471        }
472    }