001 //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.4_testing/src/org/deegree/io/datastore/sql/oracle/OracleDatastore.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 037 package org.deegree.io.datastore.sql.oracle; 038 039 import java.io.IOException; 040 import java.io.InputStream; 041 import java.lang.reflect.Field; 042 import java.sql.Connection; 043 import java.sql.PreparedStatement; 044 import java.sql.ResultSet; 045 import java.sql.SQLException; 046 import java.sql.Statement; 047 import java.util.Date; 048 import java.util.HashMap; 049 import java.util.Iterator; 050 import java.util.Map; 051 import java.util.Properties; 052 053 import oracle.spatial.geometry.JGeometry; 054 import oracle.sql.STRUCT; 055 import oracle.sql.TIMESTAMP; 056 057 import org.deegree.datatypes.Types; 058 import org.deegree.datatypes.UnknownTypeException; 059 import org.deegree.framework.log.ILogger; 060 import org.deegree.framework.log.LoggerFactory; 061 import org.deegree.framework.util.TimeTools; 062 import org.deegree.i18n.Messages; 063 import org.deegree.io.datastore.Datastore; 064 import org.deegree.io.datastore.DatastoreException; 065 import org.deegree.io.datastore.schema.MappedFeatureType; 066 import org.deegree.io.datastore.schema.MappedGeometryPropertyType; 067 import org.deegree.io.datastore.schema.TableRelation; 068 import org.deegree.io.datastore.schema.content.ConstantContent; 069 import org.deegree.io.datastore.schema.content.FieldContent; 070 import org.deegree.io.datastore.schema.content.FunctionParam; 071 import org.deegree.io.datastore.schema.content.MappingGeometryField; 072 import org.deegree.io.datastore.schema.content.SQLFunctionCall; 073 import org.deegree.io.datastore.sql.AbstractSQLDatastore; 074 import org.deegree.io.datastore.sql.StatementBuffer; 075 import org.deegree.io.datastore.sql.TableAliasGenerator; 076 import org.deegree.io.datastore.sql.VirtualContentProvider; 077 import org.deegree.io.datastore.sql.StatementBuffer.StatementArgument; 078 import org.deegree.io.datastore.sql.wherebuilder.WhereBuilder; 079 import org.deegree.model.crs.CoordinateSystem; 080 import org.deegree.model.feature.FeatureCollection; 081 import org.deegree.model.filterencoding.Filter; 082 import org.deegree.model.spatialschema.Geometry; 083 import org.deegree.model.spatialschema.GeometryException; 084 import org.deegree.ogcbase.SortProperty; 085 import org.deegree.ogcwebservices.wfs.operation.Query; 086 087 /** 088 * {@link Datastore} implementation for Oracle Spatial database systems. Supports Oracle Spatial for Oracle 10g. 089 * 090 * TODO Which Oracle spatial versions are supported exactly? 091 * 092 * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a> 093 * @author <a href="mailto:tfr@users.sourceforge.net">Torsten Friebe </a> 094 * @author last edited by: $Author: hrubach $ 095 * 096 * @version $Revision: 23693 $, $Date: 2010-04-20 14:33:55 +0200 (Di, 20. Apr 2010) $ 097 */ 098 public class OracleDatastore extends AbstractSQLDatastore { 099 100 private static final ILogger LOG = LoggerFactory.getLogger( OracleDatastore.class ); 101 102 private static final String SRS_CODE_PROP_FILE = "srs_codes_oracle.properties"; 103 104 private static Map<String, Integer> nativeSrsCodeMap = new HashMap<String, Integer>(); 105 106 // used for identifying the last active connection (if changed, Reijer's JGeometry store workaround patch is 107 // applied) 108 private static Connection lastStoreConnection; 109 110 static { 111 try { 112 initSRSCodeMap(); 113 } catch ( IOException e ) { 114 String msg = "Cannot load native srs code file '" + SRS_CODE_PROP_FILE + "'."; 115 LOG.logError( msg, e ); 116 } 117 } 118 119 /** 120 * @param code 121 * an EPSG code 122 * @return the oracle code as stored in srs_codes_oracle.properties 123 */ 124 public static int getOracleSRIDCode( String code ) { 125 Integer res = nativeSrsCodeMap.get( code ); 126 if ( res != null ) { 127 return res.intValue(); 128 } 129 130 // only in Oracle 10, but what else to do? 131 return Integer.parseInt( code.split( ":" )[1] ); 132 } 133 134 /** 135 * @param srid 136 * @return an EPSG code or "-1", if none was found 137 */ 138 public static String fromOracleSRIDCode( int srid ) { 139 for ( String k : nativeSrsCodeMap.keySet() ) { 140 if ( nativeSrsCodeMap.get( k ).intValue() == srid ) { 141 return k; 142 } 143 } 144 145 return "-1"; 146 } 147 148 /** 149 * Returns a specific {@link WhereBuilder} implementation for Oracle Spatial. 150 * 151 * @param rootFts 152 * involved (requested) feature types 153 * @param aliases 154 * aliases for the feature types, may be null 155 * @param filter 156 * filter that restricts the matched features 157 * @param sortProperties 158 * sort criteria for the result, may be null or empty 159 * @param aliasGenerator 160 * used to generate unique table aliases 161 * @param vcProvider 162 * @return <code>WhereBuilder</code> implementation for Oracle Spatial 163 * @throws DatastoreException 164 */ 165 @Override 166 public WhereBuilder getWhereBuilder( MappedFeatureType[] rootFts, String[] aliases, Filter filter, 167 SortProperty[] sortProperties, TableAliasGenerator aliasGenerator, 168 VirtualContentProvider vcProvider ) 169 throws DatastoreException { 170 return new OracleSpatialWhereBuilder( rootFts, aliases, filter, sortProperties, aliasGenerator, vcProvider ); 171 } 172 173 /** 174 * Converts an Oracle specific geometry <code>Object</code> from the <code>ResultSet</code> to a deegree 175 * <code>Geometry</code>. 176 * 177 * @param value 178 * @param targetCS 179 * @param conn 180 * @return corresponding deegree geometry 181 * @throws SQLException 182 */ 183 @Override 184 public Geometry convertDBToDeegreeGeometry( Object value, CoordinateSystem targetCS, Connection conn ) 185 throws SQLException { 186 Geometry geometry = null; 187 if ( value != null ) { 188 LOG.logDebug( "Converting STRUCT to JGeometry." ); 189 JGeometry jGeometry = JGeometry.load( (STRUCT) value ); 190 try { 191 LOG.logDebug( "Converting JGeometry to deegree geometry ('" + targetCS + "')" ); 192 geometry = JGeometryAdapter.wrap( jGeometry, targetCS ); 193 } catch ( Exception e ) { 194 LOG.logError( "Error while converting STRUCT to Geometry: ", e ); 195 throw new SQLException( "Error converting STRUCT to Geometry: " + e.getMessage() ); 196 } 197 } 198 return geometry; 199 } 200 201 /** 202 * Converts a deegree <code>Geometry</code> to an Oracle specific geometry object. 203 * 204 * @param geometry 205 * @param nativeSRSCode 206 * @param conn 207 * @return corresponding Oracle specific geometry object 208 * @throws DatastoreException 209 */ 210 @Override 211 public STRUCT convertDeegreeToDBGeometry( Geometry geometry, int nativeSRSCode, Connection conn ) 212 throws DatastoreException { 213 214 JGeometry jGeometry = null; 215 LOG.logDebug( "Converting deegree geometry to JGeometry." ); 216 try { 217 jGeometry = JGeometryAdapter.export( geometry, nativeSRSCode ); 218 } catch ( GeometryException e ) { 219 throw new DatastoreException( "Error converting deegree geometry to JGeometry: " + e.getMessage(), e ); 220 } 221 222 LOG.logDebug( "Converting JGeometry to STRUCT." ); 223 STRUCT struct = null; 224 try { 225 struct = storeGeometryWithMultiConnHack( jGeometry, conn ); 226 } catch ( SQLException e ) { 227 throw new DatastoreException( "Error converting JGeometry to STRUCT: " + e.getMessage(), e ); 228 } 229 return struct; 230 } 231 232 /** 233 * Workaround for a known Oracle JDBC driver problem. 234 * <p> 235 * JGeometry#store() isn't working when invoked successively using different connections. This method applies a 236 * workaround (based on undocumented behaviour of the Oracle driver) to solve this problem. 237 * http://forums.oracle.com/forums/thread.jspa?messageID=1273670 238 * </p> 239 * 240 * @param geometry 241 * geometry to be stored 242 * @param connection 243 * jdbc connection 244 * @return a {@link STRUCT} to be used as query parameter 245 * @throws SQLException 246 */ 247 private STRUCT storeGeometryWithMultiConnHack( JGeometry geometry, Connection connection ) 248 throws SQLException { 249 synchronized ( JGeometry.class ) { 250 if ( lastStoreConnection != null && lastStoreConnection != connection ) { 251 LOG.logDebug( "JGeometry#store(...) workaround (lastStoreConnection != connection)" ); 252 try { 253 Field geomDesc = JGeometry.class.getDeclaredField( "geomDesc" ); 254 geomDesc.setAccessible( true ); 255 geomDesc.set( null, null ); 256 } catch ( Exception e ) { 257 LOG.logWarning( "Exception caught applying JGeometr#store(...) workaround: " + e.getMessage(), e ); 258 } 259 } 260 lastStoreConnection = connection; 261 return JGeometry.store( geometry, connection ); 262 } 263 } 264 265 /** 266 * Converts the given object from a <code>java.sql.ResultSet</code> column to the common type to be used as a 267 * feature property. 268 * <p> 269 * NOTE: String- and boolean-valued results have a special conversion handling: 270 * <ul> 271 * <li><code>Strings:</code> because we encountered difficulties when inserting empty strings "" into String-type 272 * columns with NOT NULL constraints (for example in VARCHAR2 fields), "$EMPTY_STRING$" is used to mark them.</li> 273 * <li><code>Boolean:<code>because Oracle has no special boolean type, it is assumed that a CHAR(1) column is used 274 * instead (with values 'Y'=true and 'N'=false)</li> 275 * </ul> 276 * 277 * @param rsObject 278 * @param sqlTypeCode 279 * @return an object that is suitable for a table column of the specified SQL type 280 * @throws DatastoreException 281 */ 282 @Override 283 public Object convertFromDBType( Object rsObject, int sqlTypeCode ) 284 throws DatastoreException { 285 Object propertyValue = rsObject; 286 try { 287 if ( rsObject instanceof TIMESTAMP ) { 288 propertyValue = ( (TIMESTAMP) rsObject ).timestampValue(); 289 } 290 if ( rsObject instanceof String ) { 291 if ( rsObject.equals( "$EMPTY_STRING$" ) ) { 292 propertyValue = ""; 293 } 294 if ( sqlTypeCode == Types.BOOLEAN ) { 295 String val = rsObject.toString(); 296 297 if ( val.length() == 1 && val.charAt( 0 ) == 'Y' ) { 298 propertyValue = Boolean.TRUE; 299 } 300 if ( val.length() == 1 && val.charAt( 0 ) == 'N' ) { 301 propertyValue = Boolean.FALSE; 302 } 303 } 304 } 305 } catch ( SQLException e ) { 306 throw new DatastoreException( e.getMessage(), e ); 307 } 308 return propertyValue; 309 } 310 311 /** 312 * Returns the next value of the given SQL sequence. 313 * 314 * @param conn 315 * JDBC connection to be used. 316 * @param sequence 317 * name of the SQL sequence 318 * @return next value of the given SQL sequence 319 * @throws DatastoreException 320 * if the value could not be retrieved 321 */ 322 @Override 323 public Object getSequenceNextVal( Connection conn, String sequence ) 324 throws DatastoreException { 325 326 Object nextVal = null; 327 Statement stmt = null; 328 ResultSet rs = null; 329 330 try { 331 try { 332 stmt = conn.createStatement(); 333 rs = stmt.executeQuery( "SELECT " + sequence + ".nextval FROM dual" ); 334 if ( rs.next() ) { 335 nextVal = rs.getObject( 1 ); 336 } 337 } finally { 338 try { 339 if ( rs != null ) { 340 rs.close(); 341 } 342 } finally { 343 if ( stmt != null ) { 344 stmt.close(); 345 } 346 } 347 } 348 } catch ( SQLException e ) { 349 String msg = "Could not retrieve value for sequence '" + sequence + "': " + e.getMessage(); 350 throw new DatastoreException( msg, e ); 351 } 352 return nextVal; 353 } 354 355 /** 356 * Returns the current value (plus an offset) of the given SQL sequence. 357 * 358 * @param conn 359 * JDBC connection to be used. 360 * @param sequence 361 * name of the SQL sequence 362 * @param offset 363 * offset added to the sequence value 364 * @return current value (plus offset) of the given SQL sequence 365 * @throws DatastoreException 366 * if the value could not be retrieved 367 */ 368 @Override 369 public Object getSequenceCurrValPlusOffset( Connection conn, String sequence, int offset ) 370 throws DatastoreException { 371 372 Object nextVal = null; 373 Statement stmt = null; 374 ResultSet rs = null; 375 376 try { 377 try { 378 stmt = conn.createStatement(); 379 rs = stmt.executeQuery( "SELECT " + sequence + ".currval + " + offset + " FROM dual" ); 380 if ( rs.next() ) { 381 nextVal = rs.getObject( 1 ); 382 } 383 } finally { 384 try { 385 if ( rs != null ) { 386 rs.close(); 387 } 388 } finally { 389 if ( stmt != null ) { 390 stmt.close(); 391 } 392 } 393 } 394 } catch ( SQLException e ) { 395 String msg = "Could not retrieve value for sequence '" + sequence + "': " + e.getMessage(); 396 throw new DatastoreException( msg, e ); 397 } 398 return nextVal; 399 } 400 401 /** 402 * Converts the {@link StatementBuffer} into a {@link PreparedStatement}, which is initialized and ready to be 403 * performed. 404 * 405 * TODO remove this method (use super class method instead), change handling of JGeometry NOTE: String- and 406 * boolean-valued results have a special conversion handling: 407 * <ul> 408 * <li><code>Strings:</code> because we encountered difficulties when inserting empty strings "" into String-type 409 * columns with NOT NULL constraints (for example in VARCHAR2 fields), "$EMPTY_STRING$" is used to mark them.</li> 410 * <li><code>Boolean:<code>because Oracle has no special boolean type, it is assumed that a CHAR(1) column is used 411 * instead (with values 'Y'=true and 'N'=false)</li> 412 * </ul> 413 * 414 * @param conn 415 * connection to be used to create the <code>PreparedStatement</code> 416 * @param statementBuffer 417 * @return the <code>PreparedStatment</code>, ready to be performed 418 * @throws SQLException 419 * if a JDBC related error occurs 420 */ 421 @Override 422 public PreparedStatement prepareStatement( Connection conn, StatementBuffer statementBuffer ) 423 throws SQLException { 424 LOG.logDebug( "Preparing statement: " + statementBuffer.getQueryString() ); 425 426 PreparedStatement preparedStatement = conn.prepareStatement( statementBuffer.getQueryString() ); 427 428 Iterator<StatementArgument> it = statementBuffer.getArgumentsIterator(); 429 int i = 1; 430 while ( it.hasNext() ) { 431 StatementArgument argument = it.next(); 432 Object parameter = argument.getArgument(); 433 int targetSqlType = argument.getTypeCode(); 434 if ( parameter != null ) { 435 if ( targetSqlType == Types.DATE ) { 436 if ( parameter instanceof String ) { 437 parameter = TimeTools.createCalendar( (String) parameter ).getTime(); 438 } 439 parameter = new java.sql.Date( ( (Date) parameter ).getTime() ); 440 } else if ( targetSqlType == Types.TIMESTAMP ) { 441 if ( parameter instanceof String ) { 442 parameter = TimeTools.createCalendar( (String) parameter ).getTime(); 443 } 444 parameter = new java.sql.Timestamp( ( (Date) parameter ).getTime() ); 445 } else if ( parameter != null && parameter instanceof JGeometry ) { 446 parameter = storeGeometryWithMultiConnHack( (JGeometry) parameter, conn ); 447 } else if ( targetSqlType == Types.INTEGER || targetSqlType == Types.SMALLINT 448 || targetSqlType == Types.TINYINT ) { 449 parameter = Integer.parseInt( parameter.toString() ); 450 } else if ( targetSqlType == Types.DECIMAL || targetSqlType == Types.DOUBLE 451 || targetSqlType == Types.REAL || targetSqlType == Types.FLOAT ) { 452 parameter = Double.parseDouble( parameter.toString() ); 453 } else if ( targetSqlType == Types.NUMERIC ) { 454 try { 455 parameter = Integer.parseInt( parameter.toString() ); 456 } catch ( Exception e ) { 457 parameter = Double.parseDouble( parameter.toString() ); 458 } 459 } else if ( targetSqlType == Types.BOOLEAN ) { 460 // Oracle does not have a BOOLEAN datatype 461 // default maping to column of type CHAR(1) 462 // http://thinkoracle.blogspot.com/2005/07/oracle-boolean.html 463 targetSqlType = Types.CHAR; 464 if ( Boolean.parseBoolean( parameter.toString() ) ) { 465 parameter = "Y"; 466 } else { 467 parameter = "N"; 468 } 469 } else if ( parameter instanceof String ) { 470 // Using the empty string ("") for NOT NULL columns fails 471 // (at least using PreparedStatements) 472 // TODO implement a proper solution 473 if ( ( (String) parameter ).length() == 0 ) { 474 parameter = "$EMPTY_STRING$"; 475 } 476 } 477 if ( LOG.getLevel() == ILogger.LOG_DEBUG ) { 478 try { 479 String typeName = Types.getTypeNameForSQLTypeCode( targetSqlType ); 480 LOG.logDebug( "Setting argument " + i + ": type=" + typeName + ", value class=" 481 + parameter.getClass() ); 482 if ( parameter instanceof String || parameter instanceof Number 483 || parameter instanceof java.sql.Date ) { 484 LOG.logDebug( "Value: '" + parameter + "'" ); 485 } 486 } catch ( UnknownTypeException e ) { 487 throw new SQLException( e.getMessage() ); 488 } 489 } 490 preparedStatement.setObject( i, parameter, targetSqlType ); 491 } else { 492 setNullValue( preparedStatement, i, targetSqlType ); 493 } 494 i++; 495 } 496 return preparedStatement; 497 } 498 499 /** 500 * Transforms the incoming {@link Query} so that the {@link CoordinateSystem} of all spatial arguments (BBOX, etc.) 501 * in the {@link Filter} match the SRS of the targeted {@link MappingGeometryField}s. 502 * <p> 503 * NOTE: If this transformation can be performed by the backend (e.g. by Oracle Spatial), this method should be 504 * overwritten to return the original input {@link Query}. 505 * 506 * @param query 507 * query to be transformed 508 * @return query with spatial arguments transformed to target SRS 509 */ 510 @Override 511 protected Query transformQuery( Query query ) { 512 return query; 513 } 514 515 /** 516 * Transforms the {@link FeatureCollection} so that the geometries of all contained geometry properties use the 517 * requested SRS. 518 * 519 * @param fc 520 * feature collection to be transformed 521 * @param targetSRS 522 * requested SRS 523 * @return transformed FeatureCollection 524 */ 525 @Override 526 protected FeatureCollection transformResult( FeatureCollection fc, String targetSRS ) { 527 return fc; 528 } 529 530 /** 531 * Returns whether the datastore is capable of performing a native coordinate transformation (using an SQL function 532 * call for example) into the given SRS. 533 * 534 * @param targetSRS 535 * target spatial reference system (usually "EPSG:XYZ") 536 * @return true, if the datastore can perform the coordinate transformation, false otherwise 537 */ 538 @Override 539 protected boolean canTransformTo( String targetSRS ) { 540 return getNativeSRSCode( targetSRS ) != SRS_UNDEFINED; 541 } 542 543 /** 544 * Returns an {@link SQLFunctionCall} that refers to the given {@link MappingGeometryField} in the specified target 545 * SRS using a database specific SQL function. 546 * 547 * @param geoProperty 548 * geometry property 549 * @param targetSRS 550 * target spatial reference system (usually "EPSG:XYZ") 551 * @return an {@link SQLFunctionCall} that refers to the geometry in the specified srs 552 * @throws DatastoreException 553 */ 554 @Override 555 public SQLFunctionCall buildSRSTransformCall( MappedGeometryPropertyType geoProperty, String targetSRS ) 556 throws DatastoreException { 557 558 int nativeSRSCode = getNativeSRSCode( targetSRS ); 559 if ( nativeSRSCode == SRS_UNDEFINED ) { 560 String msg = Messages.getMessage( "DATASTORE_SQL_NATIVE_CT_UNKNOWN_SRS", this.getClass().getName(), 561 targetSRS ); 562 throw new DatastoreException( msg ); 563 } 564 565 MappingGeometryField field = geoProperty.getMappingField(); 566 FunctionParam param1 = new FieldContent( field, new TableRelation[0] ); 567 FunctionParam param2 = new ConstantContent( "" + nativeSRSCode ); 568 569 SQLFunctionCall transformCall = new SQLFunctionCall( "SDO_CS.TRANSFORM($1,$2)", field.getType(), param1, param2 ); 570 return transformCall; 571 } 572 573 @Override 574 public String buildSRSTransformCall( String geomIdentifier, int nativeSRSCode ) 575 throws DatastoreException { 576 String call = "SDO_CS.TRANSFORM(" + geomIdentifier + "," + nativeSRSCode + ")"; 577 return call; 578 } 579 580 @Override 581 public int getNativeSRSCode( String srsName ) { 582 Integer nativeSRSCode = nativeSrsCodeMap.get( srsName ); 583 if ( nativeSRSCode == null ) { 584 return SRS_UNDEFINED; 585 } 586 return nativeSRSCode; 587 } 588 589 private void setNullValue( PreparedStatement preparedStatement, int i, int targetSqlType ) 590 throws SQLException { 591 if ( LOG.getLevel() == ILogger.LOG_DEBUG ) { 592 try { 593 String typeName = Types.getTypeNameForSQLTypeCode( targetSqlType ); 594 LOG.logDebug( "Setting argument " + i + ": type=" + typeName ); 595 LOG.logDebug( "Value: null" ); 596 } catch ( UnknownTypeException e ) { 597 throw new SQLException( e.getMessage() ); 598 } 599 } 600 preparedStatement.setNull( i, targetSqlType ); 601 } 602 603 private static void initSRSCodeMap() 604 throws IOException { 605 InputStream is = OracleDatastore.class.getResourceAsStream( SRS_CODE_PROP_FILE ); 606 Properties props = new Properties(); 607 props.load( is ); 608 for ( Object key : props.keySet() ) { 609 String nativeCodeStr = props.getProperty( (String) key ).trim(); 610 try { 611 int nativeCode = Integer.parseInt( nativeCodeStr ); 612 nativeSrsCodeMap.put( (String) key, nativeCode ); 613 } catch ( NumberFormatException e ) { 614 String msg = Messages.getMessage( "DATASTORE_SRS_CODE_INVALID", SRS_CODE_PROP_FILE, nativeCodeStr, key ); 615 throw new IOException( msg ); 616 } 617 } 618 } 619 }