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