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