001    //$HeadURL: svn+ssh://jwilden@svn.wald.intevation.org/deegree/base/branches/2.5_testing/src/org/deegree/io/datastore/sql/postgis/PostGISWhereBuilder.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.postgis;
038    
039    import static org.deegree.i18n.Messages.getMessage;
040    
041    import java.sql.Types;
042    import java.util.ArrayList;
043    import java.util.List;
044    
045    import org.deegree.i18n.Messages;
046    import org.deegree.io.datastore.DatastoreException;
047    import org.deegree.io.datastore.schema.MappedFeatureType;
048    import org.deegree.io.datastore.schema.MappedGeometryPropertyType;
049    import org.deegree.io.datastore.sql.StatementBuffer;
050    import org.deegree.io.datastore.sql.TableAliasGenerator;
051    import org.deegree.io.datastore.sql.VirtualContentProvider;
052    import org.deegree.io.datastore.sql.wherebuilder.SpecialCharString;
053    import org.deegree.io.datastore.sql.wherebuilder.WhereBuilder;
054    import org.deegree.model.crs.CRSFactory;
055    import org.deegree.model.crs.UnknownCRSException;
056    import org.deegree.model.filterencoding.DBFunction;
057    import org.deegree.model.filterencoding.Expression;
058    import org.deegree.model.filterencoding.Filter;
059    import org.deegree.model.filterencoding.FilterEvaluationException;
060    import org.deegree.model.filterencoding.Function;
061    import org.deegree.model.filterencoding.OperationDefines;
062    import org.deegree.model.filterencoding.PropertyIsLikeOperation;
063    import org.deegree.model.filterencoding.SpatialOperation;
064    import org.deegree.model.spatialschema.Envelope;
065    import org.deegree.model.spatialschema.Geometry;
066    import org.deegree.model.spatialschema.GeometryException;
067    import org.deegree.ogcbase.SortProperty;
068    import org.postgis.PGboxbase;
069    import org.postgis.PGgeometry;
070    
071    /**
072     * {@link WhereBuilder} implementation for PostGIS databases.
073     * 
074     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </A>
075     * @author last edited by: $Author: mschneider $
076     * 
077     * @version $Revision: 23694 $, $Date: 2010-04-20 14:47:40 +0200 (Di, 20 Apr 2010) $
078     */
079    class PostGISWhereBuilder extends WhereBuilder {
080    
081        private PostGISDatastore ds;
082    
083        /**
084         * Creates a new instance of <code>PostGISWhereBuilder</code> from the given parameters.
085         * 
086         * @param rootFts
087         *            selected feature types, more than one type means that the types are joined
088         * @param aliases
089         *            aliases for the feature types, may be null (must have same length as rootFts otherwise)
090         * @param filter
091         *            filter that restricts the matched features
092         * @param sortProperties
093         *            sort criteria for the result, may be null or empty
094         * @param aliasGenerator
095         *            used to generate unique table aliases
096         * @param vcProvider
097         * @throws DatastoreException
098         */
099        public PostGISWhereBuilder( MappedFeatureType[] rootFts, String[] aliases, Filter filter,
100                                    SortProperty[] sortProperties, TableAliasGenerator aliasGenerator,
101                                    VirtualContentProvider vcProvider ) throws DatastoreException {
102            super( rootFts, aliases, filter, sortProperties, aliasGenerator, vcProvider );
103            this.ds = (PostGISDatastore) rootFts[0].getGMLSchema().getDatastore();
104        }
105    
106        /**
107         * Appends an SQL fragment for the given object to the given sql statement.
108         * 
109         * NOTE: Currently, the method uses a quirk and appends the generated argument inline, i.e. not using
110         * query.addArgument(). This is because of a problem that occurred in PostgreSQL; the execution of the inline
111         * version is *much* faster (at least with version 8.0).
112         * 
113         * @param query
114         * @param operation
115         * @throws FilterEvaluationException
116         */
117        @Override
118        protected void appendPropertyIsLikeOperationAsSQL( StatementBuffer query, PropertyIsLikeOperation operation )
119                                throws FilterEvaluationException {
120    
121            String literal = operation.getLiteral().getValue();
122            String escape = "" + operation.getEscapeChar();
123            String wildCard = "" + operation.getWildCard();
124            String singleChar = "" + operation.getSingleChar();
125    
126            SpecialCharString specialString = new SpecialCharString( literal, wildCard, singleChar, escape );
127            String sqlEncoded = specialString.toSQLStyle( !operation.isMatchCase() );
128    
129            int targetSqlType = getPropertyNameSQLType( operation.getPropertyName() );
130    
131            // if isMatchCase == false surround first argument with LOWER (...) and convert characters
132            // in second argument to lower case
133            if ( operation.isMatchCase() ) {
134                appendPropertyNameAsSQL( query, operation.getPropertyName() );
135            } else {
136                List<Expression> list = new ArrayList<Expression>();
137                list.add( operation.getPropertyName() );
138                Function func = new DBFunction( getFunctionName( "LOWER" ), list );
139                appendFunctionAsSQL( query, func, targetSqlType );
140            }
141    
142            query.append( "::VARCHAR LIKE '" );
143            query.append( sqlEncoded );
144            query.append( "'" );
145        }
146    
147        /**
148         * Generates an SQL-fragment for the given object.
149         * 
150         * TODO: Implement BBOX faster using explicit B0X-constructor
151         * 
152         * @throws DatastoreException
153         */
154        @Override
155        protected void appendSpatialOperationAsSQL( StatementBuffer query, SpatialOperation operation )
156                                throws DatastoreException {
157    
158            try {
159                switch ( operation.getOperatorId() ) {
160                case OperationDefines.BBOX: {
161                    appendBBOXOperationAsSQL( query, operation );
162                    break;
163                }
164                case OperationDefines.INTERSECTS: {
165                    appendIntersectsOperationAsSQL( query, operation );
166                    break;
167                }
168                case OperationDefines.CROSSES: {
169                    appendSimpleOperationAsSQL( query, operation, "crosses" );
170                    break;
171                }
172                case OperationDefines.EQUALS: {
173                    appendSimpleOperationAsSQL( query, operation, "equals" );
174                    break;
175                }
176                case OperationDefines.WITHIN: {
177                    appendSimpleOperationAsSQL( query, operation, "within" );
178                    break;
179                }
180                case OperationDefines.OVERLAPS: {
181                    appendSimpleOperationAsSQL( query, operation, "overlaps" );
182                    break;
183                }
184                case OperationDefines.TOUCHES: {
185                    appendSimpleOperationAsSQL( query, operation, "touches" );
186                    break;
187                }
188                case OperationDefines.DISJOINT: {
189                    appendSimpleOperationAsSQL( query, operation, "disjoint" );
190                    break;
191                }
192                case OperationDefines.CONTAINS: {
193                    appendSimpleOperationAsSQL( query, operation, "contains" );
194                    break;
195                }
196                case OperationDefines.DWITHIN: {
197                    appendDWithinOperationAsSQL( query, operation );
198                    break;
199                }
200                case OperationDefines.BEYOND: {
201                    appendBeyondOperationAsSQL( query, operation );
202                    break;
203                }
204                default: {
205                    String msg = "Spatial operator " + OperationDefines.getNameById( operation.getOperatorId() )
206                                 + " is not supported by '" + this.getClass().toString() + "'.";
207                    throw new DatastoreException( msg );
208                }
209                }
210            } catch ( GeometryException e ) {
211                throw new DatastoreException( e );
212            }
213    
214        }
215    
216        private void appendSimpleOperationAsSQL( StatementBuffer query, SpatialOperation operation, String operationName )
217                                throws GeometryException, DatastoreException {
218            query.append( operationName );
219            query.append( "(" );
220            appendPropertyNameAsSQL( query, operation.getPropertyName() );
221            query.append( ',' );
222            appendGeometryArgument( query, this.getGeometryProperty( operation.getPropertyName() ), operation.getGeometry() );
223            query.append( ')' );
224        }
225    
226        private void appendIntersectsOperationAsSQL( StatementBuffer query, SpatialOperation operation )
227                                throws GeometryException, DatastoreException {
228    
229            Envelope env = operation.getGeometry().getEnvelope();
230            MappedGeometryPropertyType geoProperty = this.getGeometryProperty( operation.getPropertyName() );
231    
232            String argumentSRS = null;
233            if ( env.getCoordinateSystem() != null ) {
234                argumentSRS = env.getCoordinateSystem().getIdentifier();
235            }
236            String propertySRS = geoProperty.getCS().getIdentifier();
237            int internalSRS = geoProperty.getMappingField().getSRS();
238    
239            int createSRSCode = getArgumentSRSCode( argumentSRS, propertySRS, internalSRS );
240            PGboxbase box = PGgeometryAdapter.export( env );
241            StringBuffer bbox = new StringBuffer( 323 );
242            bbox.append( "SetSRID(?," + createSRSCode + ")" );
243    
244            int targetSRSCode = getTargetSRSCode( argumentSRS, propertySRS, internalSRS );
245            if ( targetSRSCode != SRS_UNDEFINED ) {
246                bbox = new StringBuffer( this.ds.buildSRSTransformCall( bbox.toString(), targetSRSCode ) );
247            }
248    
249            // use the bbox operator (&&) to filter using the spatial index
250            query.append( "(" );
251            appendPropertyNameAsSQL( query, operation.getPropertyName() );
252            query.append( " && " );
253            query.append( bbox.toString() );
254            query.addArgument( box, Types.OTHER );
255    
256            query.append( " AND intersects (" );
257            appendPropertyNameAsSQL( query, operation.getPropertyName() );
258            query.append( ',' );
259            appendGeometryArgument( query, getGeometryProperty( operation.getPropertyName() ), operation.getGeometry() );
260            query.append( "))" );
261        }
262    
263        private void appendBBOXOperationAsSQL( StatementBuffer query, SpatialOperation operation )
264                                throws DatastoreException, GeometryException {
265    
266            Envelope env = operation.getGeometry().getEnvelope();
267            MappedGeometryPropertyType geoProperty = this.getGeometryProperty( operation.getPropertyName() );
268    
269            String argumentSRS = null;
270            if ( env.getCoordinateSystem() != null ) {
271                argumentSRS = env.getCoordinateSystem().getIdentifier();
272            }
273            String propertySRS = geoProperty.getCS().getIdentifier();
274            int internalSRS = geoProperty.getMappingField().getSRS();
275            int createSRSCode = getArgumentSRSCode( argumentSRS, propertySRS, internalSRS );
276            PGboxbase box = PGgeometryAdapter.export( env );
277            StringBuffer bbox = new StringBuffer( 326 );
278            bbox.append( "SetSRID(?," + createSRSCode + ")" );
279    
280            int targetSRSCode = getTargetSRSCode( argumentSRS, propertySRS, internalSRS );
281            if ( argumentSRS != null && !argumentSRS.equals( propertySRS ) ) {
282                bbox = new StringBuffer( this.ds.buildSRSTransformCall( bbox.toString(), targetSRSCode ) );
283            }
284    
285            // only the && operator uses the spatial index
286            // intersects, contains etc. do not use spatial indexing!!!!
287            query.append( "(" );
288            appendPropertyNameAsSQL( query, operation.getPropertyName() );
289            query.append( " && " );
290            query.append( bbox.toString() );
291            query.addArgument( box, Types.OTHER );
292    
293            // it is necessary to add an explicit intersects as well, because the && operator only
294            // checks for intersection of the bbox with the bboxes of the geometries (and not the
295            // geometries themselves)
296            query.append( " AND intersects (" );
297            appendPropertyNameAsSQL( query, operation.getPropertyName() );
298            query.append( ',' );
299            query.append( bbox.toString() );
300            query.addArgument( box, Types.OTHER );
301            query.append( "))" );
302        }
303    
304        private void appendDWithinOperationAsSQL( StatementBuffer query, SpatialOperation operation )
305                                throws GeometryException, DatastoreException {
306            query.append( "distance(" );
307            appendPropertyNameAsSQL( query, operation.getPropertyName() );
308            query.append( ',' );
309            appendGeometryArgument( query, this.getGeometryProperty( operation.getPropertyName() ), operation.getGeometry() );
310            query.append( ")<=" );
311            query.append( "" + operation.getDistance() );
312        }
313    
314        private void appendBeyondOperationAsSQL( StatementBuffer query, SpatialOperation operation )
315                                throws GeometryException, DatastoreException {
316            query.append( "distance(" );
317            appendPropertyNameAsSQL( query, operation.getPropertyName() );
318            query.append( ',' );
319            appendGeometryArgument( query, this.getGeometryProperty( operation.getPropertyName() ), operation.getGeometry() );
320            query.append( ")>" );
321            query.append( "" + operation.getDistance() );
322        }
323    
324        /**
325         * Construct and append the geometry argument using the correct internal SRS and perform a transform call to the
326         * internal SRS of the {@link MappedGeometryPropertyType} if necessary.
327         * 
328         * @param query
329         * @param geoProperty
330         * @param geometry
331         * @throws DatastoreException
332         * @throws GeometryException
333         */
334        private void appendGeometryArgument( StatementBuffer query, MappedGeometryPropertyType geoProperty,
335                                             Geometry geometry )
336                                throws DatastoreException, GeometryException {
337    
338            String argumentSRS = null;
339            if ( geometry.getCoordinateSystem() != null ) {
340                argumentSRS = geometry.getCoordinateSystem().getIdentifier();
341            }
342            String propertySRS = geoProperty.getCS().getIdentifier();
343            int internalSRS = geoProperty.getMappingField().getSRS();
344    
345            int createSRSCode = getArgumentSRSCode( argumentSRS, propertySRS, internalSRS );
346            PGgeometry argument = PGgeometryAdapter.export( geometry, createSRSCode );
347    
348            int targetSRSCode = getTargetSRSCode( argumentSRS, propertySRS, internalSRS );
349            if ( argumentSRS != null && !argumentSRS.equals( propertySRS ) ) {
350                query.append( ds.buildSRSTransformCall( "?", targetSRSCode ) );
351            } else {
352                query.append( '?' );
353            }
354            query.addArgument( argument, Types.OTHER );
355        }
356    
357        /**
358         * Returns the internal SRS code that must be used for the creation of a geometry argument used in a spatial
359         * operator.
360         * 
361         * @param argumentSRS
362         * @param propertySRS
363         * @param internalSrs
364         * @return the internal SRS code that must be used for the creation of a geometry argument
365         * @throws DatastoreException
366         */
367        private int getArgumentSRSCode( String argumentSRS, String propertySRS, int internalSrs )
368                                throws DatastoreException {
369            int argumentSRSCode = internalSrs;
370            if ( argumentSRS == null ) {
371                argumentSRSCode = internalSrs;
372            } else if ( !propertySRS.equals( argumentSRS ) ) {
373                // normalize SRS to first identifier
374                try {
375                    argumentSRS = CRSFactory.create( argumentSRS ).getCRS().getIdentifier();
376                } catch ( UnknownCRSException e ) {
377                    throw new DatastoreException( getMessage( "DATASTORE_SRS_UNKNOWN", argumentSRS ) );
378                }
379                argumentSRSCode = this.ds.getNativeSRSCode( argumentSRS );
380                if ( argumentSRSCode == SRS_UNDEFINED ) {
381                    String msg = Messages.getMessage( "DATASTORE_SQL_NATIVE_CT_UNKNOWN_SRS",
382                                                      PostGISDatastore.class.getName(), argumentSRS );
383                    throw new DatastoreException( msg );
384                }
385            }
386            return argumentSRSCode;
387        }
388    
389        /**
390         * Returns the internal SRS code that must be used for the transform call for a geometry argument used in a spatial
391         * operator.
392         * 
393         * @param argumentSRS
394         * @param propertySRS
395         * @param internalSrs
396         * @return the internal SRS code that must be used for the transform call of a geometry argument, or -1 if no
397         *         transformation is necessary
398         */
399        private int getTargetSRSCode( String argumentSRS, String propertySRS, int internalSrs )
400                                throws DatastoreException {
401            int targetSRS = SRS_UNDEFINED;
402            if ( argumentSRS != null && !argumentSRS.equals( propertySRS ) ) {
403                if ( internalSrs == SRS_UNDEFINED ) {
404                    String msg = Messages.getMessage( "DATASTORE_SRS_NOT_SPECIFIED2", argumentSRS, propertySRS );
405                    throw new DatastoreException( msg );
406                }
407                targetSRS = internalSrs;
408            }
409            return targetSRS;
410        }
411    }