001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/tags/2.1/src/org/deegree/io/datastore/sql/wherebuilder/WhereBuilder.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2006 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.wherebuilder;
044    
045    import java.sql.Types;
046    import java.util.ArrayList;
047    import java.util.Hashtable;
048    import java.util.Iterator;
049    import java.util.List;
050    import java.util.Stack;
051    
052    import org.deegree.framework.log.ILogger;
053    import org.deegree.framework.log.LoggerFactory;
054    import org.deegree.i18n.Messages;
055    import org.deegree.io.datastore.DatastoreException;
056    import org.deegree.io.datastore.PropertyPathResolvingException;
057    import org.deegree.io.datastore.schema.MappedFeaturePropertyType;
058    import org.deegree.io.datastore.schema.MappedFeatureType;
059    import org.deegree.io.datastore.schema.MappedGeometryPropertyType;
060    import org.deegree.io.datastore.schema.MappedPropertyType;
061    import org.deegree.io.datastore.schema.MappedSimplePropertyType;
062    import org.deegree.io.datastore.schema.TableRelation;
063    import org.deegree.io.datastore.schema.content.ConstantContent;
064    import org.deegree.io.datastore.schema.content.MappingField;
065    import org.deegree.io.datastore.schema.content.SQLFunctionCall;
066    import org.deegree.io.datastore.schema.content.SimpleContent;
067    import org.deegree.io.datastore.sql.StatementBuffer;
068    import org.deegree.io.datastore.sql.TableAliasGenerator;
069    import org.deegree.io.datastore.sql.VirtualContentProvider;
070    import org.deegree.model.filterencoding.ArithmeticExpression;
071    import org.deegree.model.filterencoding.ComparisonOperation;
072    import org.deegree.model.filterencoding.ComplexFilter;
073    import org.deegree.model.filterencoding.Expression;
074    import org.deegree.model.filterencoding.ExpressionDefines;
075    import org.deegree.model.filterencoding.FeatureFilter;
076    import org.deegree.model.filterencoding.Filter;
077    import org.deegree.model.filterencoding.Function;
078    import org.deegree.model.filterencoding.Literal;
079    import org.deegree.model.filterencoding.LogicalOperation;
080    import org.deegree.model.filterencoding.Operation;
081    import org.deegree.model.filterencoding.OperationDefines;
082    import org.deegree.model.filterencoding.PropertyIsBetweenOperation;
083    import org.deegree.model.filterencoding.PropertyIsCOMPOperation;
084    import org.deegree.model.filterencoding.PropertyIsLikeOperation;
085    import org.deegree.model.filterencoding.PropertyIsNullOperation;
086    import org.deegree.model.filterencoding.PropertyName;
087    import org.deegree.model.filterencoding.SpatialOperation;
088    import org.deegree.ogcbase.PropertyPath;
089    import org.deegree.ogcbase.SortProperty;
090    
091    /**
092     * Creates SQL-WHERE clauses from OGC filter expressions (to restrict SQL statements to all stored
093     * features that match a given filter).
094     * <p>
095     * Also handles the creation of ORDER-BY clauses.
096     * 
097     * @author <a href="mailto:tfr@users.sourceforge.net">Torsten Friebe </a>
098     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a>
099     * @author last edited by: $Author: rbezema $
100     * 
101     * @version $Revision: 7216 $, $Date: 2007-05-21 12:49:48 +0200 (Mo, 21 Mai 2007) $
102     */
103    public class WhereBuilder {
104    
105        protected static final ILogger LOG = LoggerFactory.getLogger( WhereBuilder.class );
106    
107        // database specific SRS code for unspecified SRS
108        protected static final int SRS_UNDEFINED = -1;
109    
110        /** Targeted feature types. */
111        protected MappedFeatureType[] rootFts;
112    
113        /** {@link Filter} for which the corresponding WHERE-clause will be generated. */
114        protected Filter filter;
115    
116        protected SortProperty[] sortProperties;
117    
118        protected VirtualContentProvider vcProvider;
119    
120        protected QueryTableTree queryTableTree;
121    
122        protected List<PropertyPath> filterPropertyPaths = new ArrayList<PropertyPath>();
123    
124        protected List<PropertyPath> sortPropertyPaths = new ArrayList<PropertyPath>();
125    
126        private Hashtable<String, String> functionMap = new Hashtable<String, String>();
127    
128        /**
129         * Creates a new <code>WhereBuilder</code> instance.
130         * 
131         * @param rootFts
132         *            selected feature types, more than one type means that the types are joined
133         * @param aliases
134         *            aliases for the feature types, may be null (must have same length as rootFts
135         *            otherwise)
136         * @param filter
137         * @param sortProperties
138         * @param aliasGenerator
139         *            aliasGenerator to be used to generate table aliases, may be null
140         * @param vcProvider
141         * @throws DatastoreException
142         */
143        public WhereBuilder( MappedFeatureType[] rootFts, String[] aliases, Filter filter, SortProperty[] sortProperties,
144                             TableAliasGenerator aliasGenerator, VirtualContentProvider vcProvider )
145                                throws DatastoreException {
146    
147            this.rootFts = rootFts;
148            this.queryTableTree = new QueryTableTree( rootFts, aliases, aliasGenerator );
149    
150            // add filter properties to the QueryTableTree
151            this.filter = filter;
152            if ( filter != null ) {
153                assert filter instanceof ComplexFilter || filter instanceof FeatureFilter;
154                buildFilterPropertyNameMap();
155                for ( PropertyPath property : this.filterPropertyPaths ) {
156                    this.queryTableTree.addFilterProperty( property );
157                }
158                fillFunctionNameMap();
159            }
160    
161            // add sort properties to the QueryTableTree
162            this.sortProperties = sortProperties;
163            if ( sortProperties != null ) {
164                for ( SortProperty property : sortProperties ) {
165                    this.sortPropertyPaths.add( property.getSortProperty() );
166                    this.queryTableTree.addSortProperty( property.getSortProperty() );
167                }
168            }
169    
170            this.vcProvider = vcProvider;
171    
172            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
173                LOG.logDebug( "QueryTableTree:\n" + this.queryTableTree );
174            }
175        }
176    
177        /**
178         * Returns the table alias used for the specified root feature type.
179         * 
180         * @param i
181         *            index of the requested root feature type
182         * @return the alias used for the root table
183         */
184        public String getRootTableAlias( int i ) {
185            return this.queryTableTree.getRootNodes()[i].getTableAlias();
186        }
187    
188        /**
189         * Returns the associated <code>Filter</code> instance.
190         * 
191         * @return the associated <code>Filter</code> instance
192         */
193        public Filter getFilter() {
194            return this.filter;
195        }
196    
197        protected MappedGeometryPropertyType getGeometryProperty( PropertyName propName ) {
198            PropertyPath propertyPath = propName.getValue();
199            PropertyNode propertyNode = this.queryTableTree.getPropertyNode( propertyPath );
200            assert propertyNode != null;
201            assert propertyNode instanceof GeometryPropertyNode;
202            return (MappedGeometryPropertyType) propertyNode.getProperty();
203        }
204    
205        // /**
206        // * Returns the SRS of the {@link MappedGeometryPropertyType} that is identified by the given
207        // * {@link PropertyPath}.
208        // *
209        // * @param propertyPath
210        // * @return the default SRS of the geometry property type
211        // */
212        // protected String getSrs( PropertyPath propertyPath ) {
213        // PropertyNode propertyNode = this.queryTableTree.getPropertyNode( propertyPath );
214        // assert propertyNode != null;
215        // assert propertyNode instanceof GeometryPropertyNode;
216        // MappedGeometryPropertyType geoProp = (MappedGeometryPropertyType) propertyNode.getProperty();
217        // return geoProp.getSRS().toString();
218        // }
219        //
220        // /**
221        // * Returns the internal Srs of the {@link MappedGeometryPropertyType} that is identified by
222        // the
223        // * given {@link PropertyPath}.
224        // *
225        // * @param propertyPath
226        // * @return the default SRS of the geometry property type
227        // */
228        // protected int getInternalSrsCode( PropertyPath propertyPath ) {
229        // PropertyNode propertyNode = this.queryTableTree.getPropertyNode( propertyPath );
230        // assert propertyNode != null;
231        // assert propertyNode instanceof GeometryPropertyNode;
232        // MappedGeometryPropertyType geoProp = (MappedGeometryPropertyType) propertyNode.getProperty();
233        // return geoProp.getMappingField().getSRS();
234        // }
235    
236        protected int getPropertyNameSQLType( PropertyName propertyName ) {
237    
238            PropertyPath propertyPath = propertyName.getValue();
239            PropertyNode propertyNode = this.queryTableTree.getPropertyNode( propertyPath );
240            assert propertyNode != null;
241            
242            if (propertyNode == null) {
243                LOG.logDebug("Null propertyNode for propertyName: " + propertyName +" with queryTable: " + this.queryTableTree);
244            }
245    
246            MappedPropertyType propertyType = propertyNode.getProperty();
247            if ( !( propertyType instanceof MappedSimplePropertyType ) ) {
248                String msg = "Error in WhereBuilder: cannot compare against properties of type '" + propertyType.getClass()
249                             + "'.";
250                LOG.logError( msg );
251                throw new RuntimeException( msg );
252            }
253    
254            SimpleContent content = ( (MappedSimplePropertyType) propertyType ).getContent();
255            if ( !( content instanceof MappingField ) ) {
256                String msg = "Virtual properties are currently ignored in WhereBuilder#getPropertyNameSQLType(PropertyName).";
257                LOG.logError( msg );
258                return Types.VARCHAR;
259            }
260    
261            int targetSqlType = ( (MappingField) content ).getType();
262            return targetSqlType;
263        }
264    
265        protected void buildFilterPropertyNameMap()
266                                throws PropertyPathResolvingException {
267            if ( this.filter instanceof ComplexFilter ) {
268                buildPropertyNameMapFromOperation( ( (ComplexFilter) this.filter ).getOperation() );
269            } else if ( this.filter instanceof FeatureFilter ) {
270                // TODO
271                // throw new PropertyPathResolvingException( "FeatureFilter not implemented yet." );
272            }
273        }
274    
275        private void buildPropertyNameMapFromOperation( Operation operation )
276                                throws PropertyPathResolvingException {
277            switch ( OperationDefines.getTypeById( operation.getOperatorId() ) ) {
278            case OperationDefines.TYPE_SPATIAL: {
279                registerPropertyName( ( (SpatialOperation) operation ).getPropertyName() );
280                break;
281            }
282            case OperationDefines.TYPE_COMPARISON: {
283                buildPropertyNameMap( (ComparisonOperation) operation );
284                break;
285            }
286            case OperationDefines.TYPE_LOGICAL: {
287                buildPropertyNameMap( (LogicalOperation) operation );
288                break;
289            }
290            default: {
291                break;
292            }
293            }
294        }
295    
296        private void buildPropertyNameMap( ComparisonOperation operation )
297                                throws PropertyPathResolvingException {
298            switch ( operation.getOperatorId() ) {
299            case OperationDefines.PROPERTYISEQUALTO:
300            case OperationDefines.PROPERTYISLESSTHAN:
301            case OperationDefines.PROPERTYISGREATERTHAN:
302            case OperationDefines.PROPERTYISLESSTHANOREQUALTO:
303            case OperationDefines.PROPERTYISGREATERTHANOREQUALTO: {
304                buildPropertyNameMap( ( (PropertyIsCOMPOperation) operation ).getFirstExpression() );
305                buildPropertyNameMap( ( (PropertyIsCOMPOperation) operation ).getSecondExpression() );
306                break;
307            }
308            case OperationDefines.PROPERTYISLIKE: {
309                registerPropertyName( ( (PropertyIsLikeOperation) operation ).getPropertyName() );
310                break;
311            }
312            case OperationDefines.PROPERTYISNULL: {
313                buildPropertyNameMap( ( (PropertyIsNullOperation) operation ).getPropertyName() );
314                break;
315            }
316            case OperationDefines.PROPERTYISBETWEEN: {
317                buildPropertyNameMap( ( (PropertyIsBetweenOperation) operation ).getLowerBoundary() );
318                buildPropertyNameMap( ( (PropertyIsBetweenOperation) operation ).getUpperBoundary() );
319                registerPropertyName( ( (PropertyIsBetweenOperation) operation ).getPropertyName() );
320                break;
321            }
322            default: {
323                break;
324            }
325            }
326        }
327    
328        private void buildPropertyNameMap( LogicalOperation operation )
329                                throws PropertyPathResolvingException {
330            List operationList = operation.getArguments();
331            Iterator it = operationList.iterator();
332            while ( it.hasNext() ) {
333                buildPropertyNameMapFromOperation( (Operation) it.next() );
334            }
335        }
336    
337        private void buildPropertyNameMap( Expression expression )
338                                throws PropertyPathResolvingException {
339            switch ( expression.getExpressionId() ) {
340            case ExpressionDefines.PROPERTYNAME: {
341                registerPropertyName( (PropertyName) expression );
342                break;
343            }
344            case ExpressionDefines.ADD:
345            case ExpressionDefines.SUB:
346            case ExpressionDefines.MUL:
347            case ExpressionDefines.DIV: {
348                buildPropertyNameMap( ( (ArithmeticExpression) expression ).getFirstExpression() );
349                buildPropertyNameMap( ( (ArithmeticExpression) expression ).getSecondExpression() );
350                break;
351            }
352            case ExpressionDefines.FUNCTION: {
353                // TODO: What about PropertyNames used here?
354                break;
355            }
356            case ExpressionDefines.EXPRESSION:
357            case ExpressionDefines.LITERAL: {
358                break;
359            }
360            }
361        }
362    
363        private void registerPropertyName( PropertyName propertyName ) {
364            this.filterPropertyPaths.add( propertyName.getValue() );
365        }
366    
367        /**
368         * Appends the alias-qualified, comma separated list of all tables to be joined in order to
369         * represent the associated filter expression (and possibly feature type joins).
370         * <p>
371         * The list consist of left outer joins ("x LEFT OUTER JOIN y") and cross-product joins ("x,y"):
372         * <ul>
373         * <li>left outer joins are generated for each join that is necessary, because of filter
374         * expressions that target properties stored in related tables (condition joins)
375         * <li>cross-product joins are generated for all feature type root tables (feature type joins) that
376         * have not joined by filter expression joins before</li>
377         * </ul>
378         * 
379         * @param query
380         *            the list is appended to this <code>SQLStatement</code>
381         */
382        public void appendJoinTableList( StatementBuffer query ) {
383          
384            FeatureTypeNode[] rootNodes = this.queryTableTree.getRootNodes();
385            appendOuterJoinTableList( query, rootNodes[0] );
386            for ( int i = 1; i < rootNodes.length; i++ ) {
387                query.append( ',' );
388                appendOuterJoinTableList( query, rootNodes[i] );
389            }
390        }
391    
392        /**
393         * Appends the alias-qualified, comma separated list of tables to be joined (for one root
394         * feature type node). This includes the join conditions (necessary for the filter conditions),
395         * which are generated in ANSI-SQL left outer join style.
396         * 
397         * @param query
398         *            the list is appended to this <code>SQLStatement</code>
399         * @param rootNode
400         *            one root feature type node in the <code>QueryTableTree</code>
401         */
402        private void appendOuterJoinTableList( StatementBuffer query, FeatureTypeNode rootNode ) {
403    
404            query.append( rootNode.getTable() );
405            query.append( ' ' );
406            query.append( rootNode.getTableAlias() );
407            Stack<PropertyNode> propertyNodeStack = new Stack<PropertyNode>();
408            PropertyNode[] propertyNodes = rootNode.getPropertyNodes();
409            for ( int i = 0; i < propertyNodes.length; i++ ) {
410                propertyNodeStack.push( propertyNodes[i] );
411            }
412    
413            while ( !propertyNodeStack.isEmpty() ) {
414                PropertyNode currentNode = propertyNodeStack.pop();
415                String fromAlias = currentNode.getParent().getTableAlias();
416                TableRelation[] tableRelations = currentNode.getPathFromParent();
417                if ( tableRelations != null && tableRelations.length != 0 ) {
418                    String[] toAliases = currentNode.getTableAliases();
419                    appendOuterJoins( tableRelations, fromAlias, toAliases, query );
420    
421                }
422                if ( currentNode instanceof FeaturePropertyNode ) {
423                    FeaturePropertyNode featurePropertyNode = (FeaturePropertyNode) currentNode;
424                    FeatureTypeNode[] childNodes = ( (FeaturePropertyNode) currentNode ).getFeatureTypeNodes();
425                    for ( int i = 0; i < childNodes.length; i++ ) {
426                        // TODO is this way of skipping root tables o.k.?
427                        if (childNodes[i].getFtAlias() != null) {
428                            continue;
429                        }
430                        String toTable = childNodes[i].getTable();
431                        String toAlias = childNodes[i].getTableAlias();
432                        String[] pathAliases = featurePropertyNode.getTableAliases();
433                        if ( pathAliases.length == 0 ) {
434                            fromAlias = featurePropertyNode.getParent().getTableAlias();
435                        } else {
436                            fromAlias = pathAliases[pathAliases.length - 1];
437                        }
438                        MappedFeaturePropertyType content = (MappedFeaturePropertyType) featurePropertyNode.getProperty();
439                        TableRelation[] relations = content.getTableRelations();
440                        TableRelation relation = relations[relations.length - 1];
441                        appendOuterJoin( relation, fromAlias, toAlias, toTable, query );
442                        propertyNodes = childNodes[i].getPropertyNodes();
443                        for ( int j = 0; j < propertyNodes.length; j++ ) {
444                            propertyNodeStack.push( propertyNodes[j] );
445                        }
446                    }
447                }
448            }
449        }
450    
451        private void appendOuterJoins( TableRelation[] tableRelation, String fromAlias, String[] toAliases,
452                                       StatementBuffer query ) {
453            for ( int i = 0; i < toAliases.length; i++ ) {
454                String toAlias = toAliases[i];
455                appendOuterJoin( tableRelation[i], fromAlias, toAlias, query );
456                fromAlias = toAlias;
457            }
458        }
459    
460        private void appendOuterJoin( TableRelation tableRelation, String fromAlias, String toAlias, StatementBuffer query ) {
461    
462            query.append( " LEFT OUTER JOIN " );
463            query.append( tableRelation.getToTable() );
464            query.append( " " );
465            query.append( toAlias );
466            query.append( " ON " );
467    
468            MappingField[] fromFields = tableRelation.getFromFields();
469            MappingField[] toFields = tableRelation.getToFields();
470            for ( int i = 0; i < fromFields.length; i++ ) {
471                if ( toAlias.equals( "" ) ) {
472                    toAlias = tableRelation.getToTable();
473                }
474                query.append( toAlias );
475                query.append( "." );
476                query.append( toFields[i].getField() );
477                query.append( "=" );
478                if ( fromAlias.equals( "" ) ) {
479                    fromAlias = tableRelation.getFromTable();
480                }
481                query.append( fromAlias );
482                query.append( "." );
483                query.append( fromFields[i].getField() );
484                if ( i != fromFields.length - 1 ) {
485                    query.append( " AND " );
486                }
487            }
488        }
489    
490        private void appendOuterJoin( TableRelation tableRelation, String fromAlias, String toAlias, String toTable,
491                                      StatementBuffer query ) {
492    
493            query.append( " LEFT OUTER JOIN " );
494            query.append( toTable );
495            query.append( " " );
496            query.append( toAlias );
497            query.append( " ON " );
498    
499            MappingField[] fromFields = tableRelation.getFromFields();
500            MappingField[] toFields = tableRelation.getToFields();
501            for ( int i = 0; i < fromFields.length; i++ ) {
502                if ( toAlias.equals( "" ) ) {
503                    toAlias = toTable;
504                }
505                query.append( toAlias );
506                query.append( "." );
507                query.append( toFields[i].getField() );
508                query.append( "=" );
509                if ( fromAlias.equals( "" ) ) {
510                    fromAlias = tableRelation.getFromTable();
511                }
512                query.append( fromAlias );
513                query.append( "." );
514                query.append( fromFields[i].getField() );
515                if ( i != fromFields.length - 1 ) {
516                    query.append( " AND " );
517                }
518            }
519        }
520    
521        /**
522         * Appends an SQL WHERE-condition corresponding to the <code>Filter</code> to the given SQL
523         * statement.
524         * 
525         * @param query
526         * @throws DatastoreException
527         */
528        public final void appendWhereCondition( StatementBuffer query )
529                                throws DatastoreException {
530            if ( filter instanceof ComplexFilter ) {
531                query.append( " WHERE " );
532                appendComplexFilterAsSQL( query, (ComplexFilter) filter );
533            } else if ( filter instanceof FeatureFilter ) {
534                FeatureFilter featureFilter = (FeatureFilter) filter;
535                if ( featureFilter.getFeatureIds().size() > 0 ) {
536                    query.append( " WHERE " );
537                    appendFeatureFilterAsSQL( query, featureFilter );
538                }
539            } else {
540                assert false : "Unexpected filter type: " + filter.getClass();
541            }
542        }
543    
544        /**
545         * Appends an SQL "ORDER BY"-condition that corresponds to the sort properties of the query to
546         * the given SQL statement.
547         * 
548         * @param query
549         * @throws DatastoreException
550         */
551        public void appendOrderByCondition( StatementBuffer query )
552                                throws DatastoreException {
553    
554            // ignore properties that are unsuitable as sort criteria (like constant properties)
555            List<SortProperty> sortProps = new ArrayList<SortProperty>();
556    
557            if ( this.sortProperties != null && this.sortProperties.length != 0 ) {
558                for ( int i = 0; i < this.sortProperties.length; i++ ) {
559                    SortProperty sortProperty = this.sortProperties[i];
560                    PropertyPath path = sortProperty.getSortProperty();
561                    PropertyNode propertyNode = this.queryTableTree.getPropertyNode( path );
562                    MappedPropertyType pt = propertyNode.getProperty();
563                    if ( !( pt instanceof MappedSimplePropertyType ) ) {
564                        String msg = Messages.getMessage( "DATASTORE_INVALID_SORT_PROPERTY", pt.getName() );
565                        throw new DatastoreException( msg );
566                    }
567                    SimpleContent content = ( (MappedSimplePropertyType) pt ).getContent();
568                    if ( content.isSortable() ) {
569                        sortProps.add( sortProperty );
570                    } else {
571                        String msg = "Ignoring sort criterion - property '" + path.getAsString()
572                                     + "' is not suitable for sorting.";
573                        LOG.logDebug( msg );
574                    }
575                }
576            }
577    
578            if ( sortProps.size() > 0 ) {
579                query.append( " ORDER BY " );
580            }
581    
582            for ( int i = 0; i < sortProps.size(); i++ ) {
583                SortProperty sortProperty = sortProps.get( i );
584                PropertyPath path = sortProperty.getSortProperty();
585                appendPropertyPathAsSQL( query, path );
586                if ( !sortProperty.getSortOrder() ) {
587                    query.append( " DESC" );
588                }
589                if ( i != sortProps.size() - 1 ) {
590                    query.append( ',' );
591                }
592            }
593        }
594    
595        /**
596         * Appends an SQL fragment for the given object.
597         * 
598         * @param query
599         * @param filter
600         * @throws DatastoreException
601         */
602        protected void appendComplexFilterAsSQL( StatementBuffer query, ComplexFilter filter )
603                                throws DatastoreException {
604            appendOperationAsSQL( query, filter.getOperation() );
605        }
606    
607        /**
608         * Appends an SQL fragment for the given object to the given sql statement.
609         * 
610         * @param query
611         * @param operation
612         * @throws DatastoreException
613         */
614        protected void appendOperationAsSQL( StatementBuffer query, Operation operation )
615                                throws DatastoreException {
616    
617            switch ( OperationDefines.getTypeById( operation.getOperatorId() ) ) {
618            case OperationDefines.TYPE_SPATIAL: {
619                appendSpatialOperationAsSQL( query, (SpatialOperation) operation );
620                break;
621            }
622            case OperationDefines.TYPE_COMPARISON: {
623                appendComparisonOperationAsSQL( query, (ComparisonOperation) operation );
624                break;
625            }
626            case OperationDefines.TYPE_LOGICAL: {
627                appendLogicalOperationAsSQL( query, (LogicalOperation) operation );
628                break;
629            }
630            default: {
631                break;
632            }
633            }
634        }
635    
636        /**
637         * Appends an SQL fragment for the given object to the given sql statement.
638         * 
639         * @param query
640         * @param operation
641         */
642        protected void appendComparisonOperationAsSQL( StatementBuffer query, ComparisonOperation operation ) {
643            switch ( operation.getOperatorId() ) {
644            case OperationDefines.PROPERTYISEQUALTO:
645            case OperationDefines.PROPERTYISLESSTHAN:
646            case OperationDefines.PROPERTYISGREATERTHAN:
647            case OperationDefines.PROPERTYISLESSTHANOREQUALTO:
648            case OperationDefines.PROPERTYISGREATERTHANOREQUALTO: {
649                appendPropertyIsCOMPOperationAsSQL( query, (PropertyIsCOMPOperation) operation );
650                break;
651            }
652            case OperationDefines.PROPERTYISLIKE: {
653                appendPropertyIsLikeOperationAsSQL( query, (PropertyIsLikeOperation) operation );
654                break;
655            }
656            case OperationDefines.PROPERTYISNULL: {
657                appendPropertyIsNullOperationAsSQL( query, (PropertyIsNullOperation) operation );
658                break;
659            }
660            case OperationDefines.PROPERTYISBETWEEN: {
661                appendPropertyIsBetweenOperationAsSQL( query, (PropertyIsBetweenOperation) operation );
662                break;
663            }
664            }
665        }
666    
667        /**
668         * Appends an SQL fragment for the given object to the given sql statement.
669         * 
670         * @param query
671         * @param operation
672         */
673        protected void appendPropertyIsCOMPOperationAsSQL( StatementBuffer query, PropertyIsCOMPOperation operation ) {
674            Expression firstExpr = operation.getFirstExpression();
675            if ( !( firstExpr instanceof PropertyName ) ) {
676                throw new IllegalArgumentException( "First expression in a comparison must "
677                                                    + "always be a 'PropertyName' element." );
678            }
679            int targetSqlType = getPropertyNameSQLType( (PropertyName) firstExpr );
680            if ( operation.isMatchCase() ) {
681                appendExpressionAsSQL( query, firstExpr, targetSqlType );
682            } else {
683                List<Expression> list = new ArrayList<Expression>();
684                list.add( firstExpr );
685                Function func = new Function( getFunctionName( "LOWER" ), list );
686                appendFunctionAsSQL( query, func, targetSqlType );
687            }
688            switch ( operation.getOperatorId() ) {
689            case OperationDefines.PROPERTYISEQUALTO: {
690                query.append( " = " );
691                break;
692            }
693            case OperationDefines.PROPERTYISLESSTHAN: {
694                query.append( " < " );
695                break;
696            }
697            case OperationDefines.PROPERTYISGREATERTHAN: {
698                query.append( " > " );
699                break;
700            }
701            case OperationDefines.PROPERTYISLESSTHANOREQUALTO: {
702                query.append( " <= " );
703                break;
704            }
705            case OperationDefines.PROPERTYISGREATERTHANOREQUALTO: {
706                query.append( " >= " );
707                break;
708            }
709            }
710            if ( operation.isMatchCase() ) {
711                appendExpressionAsSQL( query, operation.getSecondExpression(), targetSqlType );
712            } else {
713                List<Expression> list = new ArrayList<Expression>();
714                list.add( operation.getSecondExpression() );
715                Function func = new Function( getFunctionName( "LOWER" ), list );
716                appendFunctionAsSQL( query, func, targetSqlType );
717            }
718        }
719    
720        /**
721         * Appends an SQL fragment for the given object to the given sql statement. Replacing and escape
722         * handling is based on a finite automaton with 2 states:
723         * <p>
724         * (escapeMode)
725         * <ul>
726         * <li>' is appended as \', \ is appended as \\</li>
727         * <li>every character (including the escapeChar) is simply appended</li>
728         * <li>- unset escapeMode</li>
729         * (escapeMode is false)
730         * </ul>
731         * <ul>
732         * <li>' is appended as \', \ is appended as \\</li>
733         * <li>escapeChar means: skip char, set escapeMode</li>
734         * <li>wildCard means: append %</li>
735         * <li>singleChar means: append _</li>
736         * </ul>
737         * </p>
738         * 
739         * TODO This method needs extensive testing with different DBMS and combinations of escapeChar,
740         * wildCard and singleChar - especially problematic seems escapeChar = \
741         * 
742         * NOTE: Currently, the method uses a quirk and appends the generated argument inline, i.e. not
743         * using query.addArgument(). This is because of a problem that occurred for example in
744         * Postgresql; the execution of the inline version is *much* faster (at least with version 8.0).
745         * 
746         * @param query
747         * @param operation
748         */
749        protected void appendPropertyIsLikeOperationAsSQL( StatementBuffer query, PropertyIsLikeOperation operation ) {
750    
751            String literal = operation.getLiteral().getValue();
752            char escapeChar = operation.getEscapeChar();
753            char wildCard = operation.getWildCard();
754            char singleChar = operation.getSingleChar();
755            boolean escapeMode = false;
756            int length = literal.length();
757            int targetSqlType = getPropertyNameSQLType( operation.getPropertyName() );
758            if ( operation.isMatchCase() ) {
759                appendPropertyNameAsSQL( query, operation.getPropertyName() );
760            } else {
761                List<Expression> list = new ArrayList<Expression>();
762                list.add( operation.getPropertyName() );
763                Function func = new Function( getFunctionName( "LOWER" ), list );
764                appendFunctionAsSQL( query, func, targetSqlType );
765            }
766            query.append( " LIKE '" );
767            StringBuffer parameter = new StringBuffer();
768            for ( int i = 0; i < length; i++ ) {
769                char c = literal.charAt( i );
770                if ( escapeMode ) {
771                    if ( c == '\'' ) {
772                        // ' must be converted to escapeChar + '
773                        parameter.append( escapeChar );
774                        parameter.append( '\'' );
775                    } else if ( c == '%' ) {
776                        // % must be converted to escapeChar + %
777                        parameter.append( escapeChar );
778                        parameter.append( '%' );
779                    } else if ( c == '_' ) {
780                        // _ must be converted to escapeChar + _
781                        parameter.append( escapeChar );
782                        parameter.append( '_' );
783                    } else if ( c == '\\' ) {
784                        // \ must be converted to escapeChar + \
785                        parameter.append( escapeChar );
786                        parameter.append( '\\' );
787                    } else if ( c == escapeChar ) {
788                        // escapeChar must be converted to escapeChar + escapeChar
789                        parameter.append( escapeChar );
790                        parameter.append( escapeChar );
791                    } else {
792                        parameter.append( c );
793                    }
794                    escapeMode = false;
795                } else {
796                    // escapeChar means: switch to escapeMode
797                    if ( c == escapeChar ) {
798                        escapeMode = true;
799                    } else if ( c == wildCard ) {
800                        // wildCard must be converted to %
801                        parameter.append( '%' );
802                        // singleChar must be converted to ?
803                    } else if ( c == singleChar ) {
804                        parameter.append( '_' );
805                    } else if ( c == '\'' ) {
806                        // ' must be converted to escapeChar + '
807                        parameter.append( escapeChar );
808                        parameter.append( '\'' );
809                    } else if ( c == '%' ) {
810                        // % must be converted to escapeChar + %
811                        parameter.append( escapeChar );
812                        parameter.append( '%' );
813                    } else if ( c == '_' ) {
814                        // _ must be converted to escapeChar + _
815                        parameter.append( escapeChar );
816                        parameter.append( '_' );
817                    } else if ( c == '\\' ) {
818                        // \ must (even in escapeMode) be converted to escapeChar + \
819                        parameter.append( escapeChar );
820                        parameter.append( '\\' );
821                    } else {
822                        parameter.append( c );
823                    }
824                }
825            }
826            if ( operation.isMatchCase() ) {
827                query.append( parameter );
828            } else {
829                query.append( parameter.toString().toLowerCase() );
830            }
831            query.append( "\' ESCAPE '" );
832            if ( escapeChar == '\\' || escapeChar == '\'' ) {
833                query.append( '\\' );
834            }
835            query.append( escapeChar );
836            query.append( "'" );
837            // query.addArgument( parameter.toString() );
838        }
839    
840        /**
841         * Appends an SQL fragment for the given object to the given sql statement.
842         * 
843         * @param query
844         * @param operation
845         */
846        protected void appendPropertyIsNullOperationAsSQL( StatementBuffer query, PropertyIsNullOperation operation ) {
847            appendPropertyNameAsSQL( query, operation.getPropertyName() );
848            query.append( " IS NULL" );
849        }
850    
851        /**
852         * Appends an SQL fragment for the given object to the given sql statement.
853         * 
854         * @param query
855         * @param operation
856         */
857        protected void appendPropertyIsBetweenOperationAsSQL( StatementBuffer query, PropertyIsBetweenOperation operation ) {
858    
859            PropertyName propertyName = operation.getPropertyName();
860            int targetSqlType = getPropertyNameSQLType( propertyName );
861            appendExpressionAsSQL( query, operation.getLowerBoundary(), targetSqlType );
862            query.append( " <= " );
863            appendPropertyNameAsSQL( query, propertyName );
864            query.append( " AND " );
865            appendPropertyNameAsSQL( query, propertyName );
866            query.append( " <= " );
867            appendExpressionAsSQL( query, operation.getUpperBoundary(), targetSqlType );
868        }
869    
870        /**
871         * Appends an SQL fragment for the given object to the given sql statement.
872         * 
873         * @param query
874         * @param expression
875         * @param targetSqlType
876         *            sql type code to be used for literals at the bottom of the expression tree
877         */
878        protected void appendExpressionAsSQL( StatementBuffer query, Expression expression, int targetSqlType ) {
879            switch ( expression.getExpressionId() ) {
880            case ExpressionDefines.PROPERTYNAME: {
881                appendPropertyNameAsSQL( query, (PropertyName) expression );
882                break;
883            }
884            case ExpressionDefines.LITERAL: {
885                appendLiteralAsSQL( query, (Literal) expression, targetSqlType );
886                break;
887            }
888            case ExpressionDefines.FUNCTION: {
889                Function function = (Function) expression;
890                appendFunctionAsSQL( query, function, targetSqlType );
891                break;
892            }
893            case ExpressionDefines.ADD:
894            case ExpressionDefines.SUB:
895            case ExpressionDefines.MUL:
896            case ExpressionDefines.DIV: {
897                appendArithmeticExpressionAsSQL( query, (ArithmeticExpression) expression, targetSqlType );
898                break;
899            }
900            case ExpressionDefines.EXPRESSION:
901            default: {
902                throw new IllegalArgumentException( "Unexpected expression type: " + expression.getExpressionName() );
903            }
904            }
905        }
906    
907        /**
908         * Appends an SQL fragment for the given object to the given sql statement.
909         * 
910         * @param query
911         * @param literal
912         * @param targetSqlType
913         */
914        protected void appendLiteralAsSQL( StatementBuffer query, Literal literal, int targetSqlType ) {
915            query.append( '?' );
916            query.addArgument( literal.getValue(), targetSqlType );
917        }
918    
919        /**
920         * Appends an SQL fragment for the given object to the given sql statement.
921         * 
922         * @param query
923         * @param propertyName
924         */
925        protected void appendPropertyNameAsSQL( StatementBuffer query, PropertyName propertyName ) {
926    
927            PropertyPath propertyPath = propertyName.getValue();
928            appendPropertyPathAsSQL( query, propertyPath );
929        }
930    
931        /**
932         * Appends an SQL fragment for the given object to the given sql statement.
933         * 
934         * @param query
935         * @param propertyPath
936         */
937        protected void appendPropertyPathAsSQL( StatementBuffer query, PropertyPath propertyPath ) {
938    
939            LOG.logDebug( "Looking up '" + propertyPath + "' in the query table tree." );
940            MappingField mappingField = null;
941            PropertyNode propertyNode = this.queryTableTree.getPropertyNode( propertyPath );
942            assert ( propertyNode != null );
943            if ( propertyNode instanceof SimplePropertyNode ) {
944                SimpleContent content = ( (MappedSimplePropertyType) ( propertyNode.getProperty() ) ).getContent();
945                if ( !( content instanceof MappingField ) ) {
946                    if ( content instanceof ConstantContent ) {
947                        query.append( "'" + ( (ConstantContent) content ).getValue() + "'" );
948                        return;
949                    } else if ( content instanceof SQLFunctionCall ) {
950                        SQLFunctionCall call = (SQLFunctionCall) content;
951                        String tableAlias = null;
952                        String[] tableAliases = propertyNode.getTableAliases();
953                        if ( tableAliases == null || tableAliases.length == 0 ) {
954                            tableAlias = propertyNode.getParent().getTableAlias();
955                        } else {
956                            tableAlias = tableAliases[tableAliases.length - 1];
957                        }
958                        this.vcProvider.appendSQLFunctionCall( query, tableAlias, call );
959                        return;
960                    }
961                    String msg = "Virtual properties are currently ignored in WhereBuilder#appendPropertyPathAsSQL(StatementBuffer,PropertyPath).";
962                    LOG.logError( msg );
963                    assert false;
964                }
965                mappingField = (MappingField) content;
966            } else if ( propertyNode instanceof GeometryPropertyNode ) {
967                mappingField = ( (MappedGeometryPropertyType) propertyNode.getProperty() ).getMappingField();
968            } else {
969                String msg = "Internal error in WhereBuilder: unhandled PropertyNode type: '"
970                             + propertyNode.getClass().getName() + "'.";
971                LOG.logError( msg );
972                throw new RuntimeException( msg );
973            }
974            String tableAlias = null;
975            String[] tableAliases = propertyNode.getTableAliases();
976            if ( tableAliases == null || tableAliases.length == 0 ) {
977                tableAlias = propertyNode.getParent().getTableAlias();
978            } else {
979                tableAlias = tableAliases[tableAliases.length - 1];
980            }
981            if ( tableAlias != "" ) {
982                query.append( tableAlias );
983                query.append( '.' );
984            } else {
985                query.append( mappingField.getTable() );
986                query.append( '.' );
987            }
988            query.append( mappingField.getField() );
989        }
990    
991        /**
992         * Appends an SQL fragment for the given object to the given sql statement.
993         * 
994         * @param query
995         * @param expression
996         * @param targetSqlType
997         */
998        protected void appendArithmeticExpressionAsSQL( StatementBuffer query, ArithmeticExpression expression,
999                                                        int targetSqlType ) {
1000            query.append( '(' );
1001            appendExpressionAsSQL( query, expression.getFirstExpression(), targetSqlType );
1002            switch ( expression.getExpressionId() ) {
1003            case ExpressionDefines.ADD: {
1004                query.append( '+' );
1005                break;
1006            }
1007            case ExpressionDefines.SUB: {
1008                query.append( '-' );
1009                break;
1010            }
1011            case ExpressionDefines.MUL: {
1012                query.append( '*' );
1013                break;
1014            }
1015            case ExpressionDefines.DIV: {
1016                query.append( '/' );
1017                break;
1018            }
1019            }
1020            appendExpressionAsSQL( query, expression.getSecondExpression(), targetSqlType );
1021            query.append( ')' );
1022        }
1023    
1024        /**
1025         * Appends an SQL fragment for the given object to the given sql statement.
1026         * 
1027         * @param query
1028         * @param function
1029         * @param targetSqlType
1030         */
1031        protected void appendFunctionAsSQL( StatementBuffer query, Function function, int targetSqlType ) {
1032            query.append( function.getName() );
1033            query.append( " (" );
1034            List list = function.getArguments();
1035            for ( int i = 0; i < list.size(); i++ ) {
1036                Expression expression = (Expression) list.get( i );
1037                appendExpressionAsSQL( query, expression, targetSqlType );
1038                if ( i != list.size() - 1 )
1039                    query.append( ", " );
1040            }
1041            query.append( ")" );
1042        }
1043    
1044        /**
1045         * Appends an SQL fragment for the given object to the given sql statement.
1046         * 
1047         * @param query
1048         * @param operation
1049         * @throws DatastoreException
1050         */
1051        protected void appendLogicalOperationAsSQL( StatementBuffer query, LogicalOperation operation )
1052                                throws DatastoreException {
1053            List argumentList = operation.getArguments();
1054            switch ( operation.getOperatorId() ) {
1055            case OperationDefines.AND: {
1056                for ( int i = 0; i < argumentList.size(); i++ ) {
1057                    Operation argument = (Operation) argumentList.get( i );
1058                    query.append( '(' );
1059                    appendOperationAsSQL( query, argument );
1060                    query.append( ')' );
1061                    if ( i != argumentList.size() - 1 )
1062                        query.append( " AND " );
1063                }
1064                break;
1065            }
1066            case OperationDefines.OR: {
1067                for ( int i = 0; i < argumentList.size(); i++ ) {
1068                    Operation argument = (Operation) argumentList.get( i );
1069                    query.append( '(' );
1070                    appendOperationAsSQL( query, argument );
1071                    query.append( ')' );
1072                    if ( i != argumentList.size() - 1 )
1073                        query.append( " OR " );
1074                }
1075                break;
1076            }
1077            case OperationDefines.NOT: {
1078                Operation argument = (Operation) argumentList.get( 0 );
1079                query.append( "NOT (" );
1080                appendOperationAsSQL( query, argument );
1081                query.append( ')' );
1082                break;
1083            }
1084            }
1085        }
1086    
1087        /**
1088         * Appends an SQL fragment for the given object to the given sql statement.
1089         * 
1090         * TODO Handle compound primary keys correctly.
1091         * 
1092         * @param query
1093         * @param filter
1094         * @throws DatastoreException
1095         */
1096        protected void appendFeatureFilterAsSQL( StatementBuffer query, FeatureFilter filter )
1097                                throws DatastoreException {
1098    
1099            // List list = filter.getFeatureIds();
1100            // Iterator it = list.iterator();
1101            // while (it.hasNext()) {
1102            // FeatureId fid = (FeatureId) it.next();
1103            // MappingField mapping = null;
1104            // DatastoreMapping mapping = featureType.getFidDefinition().getFidFields()[0];
1105            // query.append( ' ' );
1106            // query.append( this.joinTableTree.getAlias() );
1107            // query.append( "." );
1108            // query.append( mapping.getField() );
1109            // query.append( "=?" );
1110            // query.addArgument( fid.getValue() );
1111            // if ( it.hasNext() ) {
1112            // query.append( " OR" );
1113            // }
1114            // }
1115    
1116            if ( this.rootFts.length > 1 ) {
1117                String msg = Messages.getMessage( "DATASTORE_FEATURE_QUERY_MORE_THAN_FEATURE_TYPE" );
1118                throw new DatastoreException( msg );
1119            }
1120    
1121            MappedFeatureType rootFt = this.rootFts[0];
1122    
1123            ArrayList list = filter.getFeatureIds();
1124            MappingField mapping = rootFt.getGMLId().getIdFields()[0];
1125            query.append( ' ' );
1126            String tbl = getRootTableAlias( 0 );
1127            if ( null != tbl && 0 < tbl.length() ) {
1128                query.append( tbl );
1129                query.append( "." );
1130            }
1131            query.append( mapping.getField() );
1132            try {
1133                for ( int i = 0; i < list.size(); i++ ) {
1134                    if ( 0 == i )
1135                        query.append( " IN (?" );
1136                    else
1137                        query.append( ",?" );
1138                    String fid = ( (org.deegree.model.filterencoding.FeatureId) list.get( i ) ).getValue();
1139                    Object fidValue = org.deegree.io.datastore.FeatureId.removeFIDPrefix( fid, rootFt.getGMLId() );
1140                    query.addArgument( fidValue, mapping.getType() );
1141                }
1142            } catch ( Exception e ) {
1143                LOG.logError( "Error converting feature id", e );
1144            }
1145            query.append( ")" );
1146        }
1147    
1148        /**
1149         * Appends an SQL fragment for the given object to the given sql statement. As this depends on
1150         * the handling of geometry data by the concrete database in use, this method must be
1151         * overwritten by any datastore implementation that has spatial capabilities.
1152         * 
1153         * @param query
1154         * @param operation
1155         * @throws DatastoreException
1156         */
1157        protected void appendSpatialOperationAsSQL( @SuppressWarnings("unused")
1158        StatementBuffer query, @SuppressWarnings("unused")
1159        SpatialOperation operation )
1160                                throws DatastoreException {
1161            String msg = "Spatial operations are not supported by the WhereBuilder implementation in use: '" + getClass()
1162                         + "'";
1163            throw new DatastoreException( msg );
1164        }
1165    
1166        /**
1167         * Prepares the function map for functions with implementation specific names, e.g. upper case
1168         * conversion in ORACLE = UPPER(string); POSTGRES = UPPER(string), and MS Access =
1169         * UCase(string). Default SQL-function name map function 'UPPER' is 'UPPER'. If this function
1170         * shall be used with user databases e.g. SQLServer a specialized WhereBuilder must override
1171         * this method.
1172         */
1173        protected void fillFunctionNameMap() {
1174            functionMap.clear();
1175            functionMap.put( "LOWER", "LOWER" );
1176        }
1177    
1178        /**
1179         * Get the function with the specified name.
1180         * 
1181         * @param name
1182         *            the function name
1183         * @return the mapped function name
1184         */
1185        protected String getFunctionName( String name ) {
1186            String f = functionMap.get( name );
1187            if ( null == f )
1188                f = name;
1189            return f;
1190        }
1191    }