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