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