001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_testing/src/org/deegree/portal/portlet/modules/wfs/actions/portlets/WFSClientPortletPerform.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.portal.portlet.modules.wfs.actions.portlets;
037    
038    import java.io.BufferedReader;
039    import java.io.ByteArrayInputStream;
040    import java.io.ByteArrayOutputStream;
041    import java.io.File;
042    import java.io.FileReader;
043    import java.io.IOException;
044    import java.io.InputStream;
045    import java.io.StringReader;
046    import java.io.UnsupportedEncodingException;
047    import java.net.URL;
048    import java.nio.charset.Charset;
049    import java.util.HashMap;
050    import java.util.Iterator;
051    import java.util.Map;
052    
053    import javax.servlet.ServletContext;
054    import javax.servlet.http.HttpServletRequest;
055    
056    import org.apache.commons.httpclient.HttpClient;
057    import org.apache.commons.httpclient.methods.PostMethod;
058    import org.apache.commons.httpclient.methods.StringRequestEntity;
059    import org.apache.jetspeed.portal.Portlet;
060    import org.deegree.datatypes.Types;
061    import org.deegree.enterprise.WebUtils;
062    import org.deegree.enterprise.control.RPCException;
063    import org.deegree.enterprise.control.RPCFactory;
064    import org.deegree.enterprise.control.RPCMethodCall;
065    import org.deegree.enterprise.control.RPCParameter;
066    import org.deegree.enterprise.control.RPCStruct;
067    import org.deegree.enterprise.control.RPCUtils;
068    import org.deegree.framework.log.ILogger;
069    import org.deegree.framework.log.LoggerFactory;
070    import org.deegree.framework.util.StringTools;
071    import org.deegree.framework.xml.XMLFragment;
072    import org.deegree.framework.xml.XSLTDocument;
073    import org.deegree.model.crs.CRSFactory;
074    import org.deegree.model.crs.CoordinateSystem;
075    import org.deegree.model.crs.GeoTransformer;
076    import org.deegree.model.crs.UnknownCRSException;
077    import org.deegree.model.feature.Feature;
078    import org.deegree.model.feature.FeatureCollection;
079    import org.deegree.model.feature.FeatureFactory;
080    import org.deegree.model.feature.FeatureProperty;
081    import org.deegree.model.feature.GMLFeatureAdapter;
082    import org.deegree.model.feature.GMLFeatureCollectionDocument;
083    import org.deegree.model.feature.schema.FeatureType;
084    import org.deegree.model.spatialschema.Geometry;
085    import org.deegree.ogcwebservices.OGCWebServiceException;
086    import org.deegree.ogcwebservices.OWSUtils;
087    import org.deegree.ogcwebservices.getcapabilities.InvalidCapabilitiesException;
088    import org.deegree.ogcwebservices.wfs.capabilities.WFSCapabilities;
089    import org.deegree.ogcwebservices.wfs.capabilities.WFSCapabilitiesDocument;
090    import org.deegree.ogcwebservices.wfs.operation.GetFeature;
091    import org.deegree.portal.PortalException;
092    import org.deegree.portal.portlet.modules.actions.IGeoPortalPortletPerform;
093    
094    /**
095     *
096     *
097     * @version $Revision: 18195 $
098     * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
099     * @author last edited by: $Author: mschneider $
100     *
101     * @version 1.0. $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18. Jun 2009) $
102     *
103     * @since 2.0
104     */
105    public class WFSClientPortletPerform extends IGeoPortalPortletPerform {
106    
107        private static final ILogger LOG = LoggerFactory.getLogger( WFSClientPortletPerform.class );
108    
109        protected static final String INIT_TARGETSRS = "TARGETSRS";
110    
111        protected static final String INIT_XSLT = "XSLT";
112    
113        private static Map<String, WFSCapabilities> capaMap = new HashMap<String, WFSCapabilities>();
114    
115        /**
116         * @param request
117         * @param portlet
118         * @param servletContext
119         */
120        public WFSClientPortletPerform( HttpServletRequest request, Portlet portlet, ServletContext servletContext ) {
121            super( request, portlet, servletContext );
122    
123        }
124    
125        protected void doGetfeature()
126                                throws PortalException, OGCWebServiceException {
127    
128            RPCParameter[] rpcParams = extractRPCParameters();
129            Map<String, FeatureCollection> allFCs = new HashMap<String, FeatureCollection>();
130            for ( int i = 1; i < rpcParams.length; i++ ) {
131                // first field will be skipped because it contains informations
132                // about the desired result format
133                RPCStruct struct = (RPCStruct) rpcParams[i].getValue();
134    
135                String tmp = RPCUtils.getRpcPropertyAsString( struct, "featureTypes" );
136    
137                String[] arr = StringTools.toArray( tmp, ",", true );
138                String[] xmlns = new String[arr.length];
139                String[] featureTypes = new String[arr.length];
140                for ( int j = 0; j < arr.length; j++ ) {
141                    int p = arr[j].lastIndexOf( ':' );
142                    xmlns[j] = arr[j].substring( 0, p );
143                    featureTypes[j] = arr[j].substring( p + 1, arr[j].length() );
144                }
145    
146                for ( int j = 0; j < featureTypes.length; j++ ) {
147    
148                    String query = createQuery( struct, xmlns, featureTypes );
149    
150                    LOG.logDebug( "queried feature type: " + xmlns[j] + featureTypes[j] );
151                    LOG.logDebug( "Query: \n" + query );
152    
153                    Map<String, FeatureCollection> fcs = null;
154    
155                    try {
156                        fcs = performQuery( featureTypes[j], xmlns[j], query );
157                    } catch ( UnsupportedEncodingException e ) {
158                        LOG.logError( e.getMessage(), e );
159                        throw new PortalException( e.getMessage(), e );
160                    }
161    
162                    if ( getInitParam( INIT_TARGETSRS ) != null ) {
163                        Iterator<String> iter = fcs.keySet().iterator();
164                        while ( iter.hasNext() ) {
165                            String key = iter.next();
166                            FeatureCollection tmpFc = fcs.get( key );
167                            fcs.put( key, transformGeometries( tmpFc ) );
168                        }
169                    }
170    
171                    allFCs.putAll( fcs );
172    
173                }
174    
175            }
176            writeGetFeatureResult( allFCs, (String) rpcParams[0].getValue() );
177        }
178    
179        /**
180         * creates a WFS query depending on requested construction type
181         *
182         * @param struct
183         * @param xmlns
184         * @param featureTypes
185         * @return the query
186         * @throws PortalException
187         */
188        private String createQuery( RPCStruct struct, String[] xmlns, String[] featureTypes )
189                                throws PortalException {
190            String query = null;
191            String template = RPCUtils.getRpcPropertyAsString( struct, "queryTemplate" );
192            if ( template != null ) {
193                RPCParameter[] filterProps = null;
194                if ( struct.getMember( "filterProperties" ) != null ) {
195                    filterProps = (RPCParameter[]) struct.getMember( "filterProperties" ).getValue();
196                }
197                query = createQueryFromTemplate( template, filterProps );
198            } else if ( parameter.get( "FILTER" ) != null ) {
199                String filter = parameter.get( "FILTER" );
200                query = createQueryFromFilter( featureTypes, xmlns, filter );
201            } else {
202                String filter = createFilterFromProperties();
203                query = createQueryFromFilter( featureTypes, xmlns, filter );
204            }
205            return query;
206        }
207    
208        /**
209         * extracts the
210         *
211         * @see RPCParameter array from the RPC method call
212         * @return the array
213         * @throws PortalException
214         */
215        protected RPCParameter[] extractRPCParameters()
216                                throws PortalException {
217            String tmp = parameter.get( "RPC" );
218    
219            StringReader sr = new StringReader( tmp );
220            RPCMethodCall rpcMethod = null;
221            try {
222                rpcMethod = RPCFactory.createRPCMethodCall( sr );
223            } catch ( RPCException e ) {
224                LOG.logError( e.getMessage(), e );
225                throw new PortalException( e.getMessage() );
226            }
227    
228            RPCParameter[] rpcParams = rpcMethod.getParameters();
229            return rpcParams;
230        }
231    
232        /**
233         * performs a transaction against a WFS-T or a database. The backend type to be used by a
234         * transaction depends on a portlets initParameters.
235         *
236         */
237        public void doTransaction() {
238            System.out.println( parameter );
239            // throw new UnsupportedOperationException();
240        }
241    
242        /**
243         * writes the result into the forwarded request object
244         *
245         * @param xml
246         * @param fc
247         * @throws PortalException
248         */
249        private void writeGetFeatureResult( Map<String, FeatureCollection> fcs, String format )
250                                throws PortalException {
251            if ( "XML".equals( format ) ) {
252                XMLFragment xml = new XMLFragment();
253    
254                if ( fcs != null ) {
255                    FeatureCollection fc = FeatureFactory.createFeatureCollection( "ID", 1000 );
256                    Iterator<String> iter = fcs.keySet().iterator();
257                    while ( iter.hasNext() ) {
258                        fc.addAll( fcs.get( iter.next() ) );
259                    }
260                    ByteArrayOutputStream bos = new ByteArrayOutputStream( 100000 );
261                    try {
262                        new GMLFeatureAdapter().export( fc, bos );
263                        xml.load( new ByteArrayInputStream( bos.toByteArray() ), XMLFragment.DEFAULT_URL );
264                    } catch ( Exception e ) {
265                        LOG.logError( e.getMessage(), e );
266                        throw new PortalException( "could not export feature collection as GML", e );
267                    }
268                    if ( getInitParam( INIT_XSLT ) != null ) {
269                        xml = transform( xml );
270                    }
271                }
272    
273                request.setAttribute( "RESULT", xml );
274            } else {
275                request.setAttribute( "RESULT", fcs );
276            }
277        }
278    
279        /**
280         * transforms the result of a WFS request using the XSLT script defined by an init parameter
281         *
282         * @param xml
283         * @return the transformed XML
284         * @throws PortalException
285         */
286        private XMLFragment transform( XMLFragment xml )
287                                throws PortalException {
288            String xslF = getInitParam( INIT_XSLT );
289            File file = new File( xslF );
290            if ( !file.isAbsolute() ) {
291                file = new File( sc.getRealPath( xslF ) );
292            }
293            XSLTDocument xslt = new XSLTDocument();
294            try {
295                xslt.load( file.toURI().toURL() );
296                xml = xslt.transform( xml );
297            } catch ( Exception e ) {
298                LOG.logError( e.getMessage(), e );
299                throw new PortalException( "could not transform result of WFS request", e );
300            }
301            return xml;
302        }
303    
304        /**
305         * transforms the geometry properties of the features contained in the passed feature collection
306         * into the target CRS given by an init parameter
307         *
308         * @param fc
309         * @return the transformed feature collection
310         * @throws PortalException
311         */
312        private FeatureCollection transformGeometries( FeatureCollection fc )
313                                throws PortalException {
314            String cs = getInitParam( INIT_TARGETSRS );
315            CoordinateSystem crs;
316            try {
317                crs = CRSFactory.create( cs );
318            } catch ( UnknownCRSException e1 ) {
319                throw new PortalException( e1.getMessage(), e1 );
320            }
321            if ( crs == null ) {
322                throw new PortalException( "CRS: " + cs + " is not known by deegree" );
323            }
324            try {
325                GeoTransformer gt = new GeoTransformer( crs );
326                for ( int i = 0; i < fc.size(); i++ ) {
327                    Feature feature = fc.getFeature( i );
328                    FeatureType ft = feature.getFeatureType();
329                    FeatureProperty[] fp = feature.getProperties();
330                    for ( int j = 0; j < fp.length; j++ ) {
331                        if ( ft.getProperty( fp[j].getName() ).getType() == Types.GEOMETRY ) {
332                            Geometry geom = (Geometry) fp[j].getValue();
333                            if ( !crs.equals( geom.getCoordinateSystem() ) ) {
334                                geom = gt.transform( geom );
335                                fp[j].setValue( geom );
336                            }
337                        }
338                    }
339                }
340            } catch ( Exception e ) {
341                LOG.logError( e.getMessage(), e );
342                throw new PortalException( "could not transform geometries to target CRS: " + cs, e );
343            }
344            return fc;
345        }
346    
347        /**
348         * performs a GetFeature query against one or more WFS's
349         *
350         * @param featureType
351         * @param namespace
352         * @param query
353         * @return the map
354         * @throws OGCWebServiceException
355         * @throws UnsupportedEncodingException
356         */
357        private Map<String, FeatureCollection> performQuery( String featureType, String namespace, String query )
358                                throws OGCWebServiceException, UnsupportedEncodingException {
359            // WFS to contact
360            String addr = getInitParam( namespace + ':' + featureType );
361            if ( addr == null ) {
362                // if a client does not send the name of the target WFS
363                // 'WFS' will be used to get the target WFS address from
364                // the portlets init-parameter
365                addr = getInitParam( "WFS" );
366            }
367            if ( addr == null ) {
368                throw new OGCWebServiceException( "WFS: " + namespace + ':' + featureType + " is not known by the portal" );
369            }
370    
371            // a featuretype may be assigned to more than one WFS
372            String[] addresses = StringTools.toArray( addr, ",", false );
373            Map<String, FeatureCollection> docs = new HashMap<String, FeatureCollection>();
374            for ( int i = 0; i < addresses.length; i++ ) {
375                if ( capaMap.get( addresses[i] ) == null ) {
376                    // if the WFS Capabilities has not already been read from this
377                    // address it will be done now. The result will be stored in the
378                    // static Map 'capaMap' to be available at the next call
379                    loadWFSCapabilities( addresses[i] );
380                }
381    
382                URL url = OWSUtils.getHTTPPostOperationURL( capaMap.get( addresses[i] ), GetFeature.class );
383    
384                LOG.logDebug( "performing query: ", query );
385                StringRequestEntity re = new StringRequestEntity( query, "text/xml", Charset.defaultCharset().toString() );
386                PostMethod post = new PostMethod( url.toExternalForm() );
387                post.setRequestEntity( re );
388                InputStream is = null;
389                try {
390                    HttpClient client = new HttpClient();
391                    client = WebUtils.enableProxyUsage( client, url );
392                    client.executeMethod( post );
393                    is = post.getResponseBodyAsStream();
394                } catch ( Exception e ) {
395                    LOG.logInfo( url.toExternalForm() );
396                    LOG.logError( e.getMessage(), e );
397                    throw new OGCWebServiceException( "could not perform query against the WFS: " + namespace + ':'
398                                                      + featureType );
399                }
400                try {
401                    GMLFeatureCollectionDocument xml = new GMLFeatureCollectionDocument();
402                    xml.load( is, addresses[i] );
403                    // put the result on a Map that will be forced to the client
404                    // which is responsible for what to do with it. Because the keys
405                    // of the Map are the WFS addresses the client is able to reconstruct
406                    // the source of the result parts
407                    docs.put( addresses[i], xml.parse() );
408                } catch ( Exception e ) {
409                    LOG.logError( e.getMessage(), e );
410                    throw new OGCWebServiceException( "could not parse response from WFS: " + namespace + ':' + featureType
411                                                      + " as XML" );
412                }
413            }
414            return docs;
415        }
416    
417        /**
418         * performs a GetCapabilities request against the passed address and stores the result (if it is
419         * a valid WFS capabilities document) in a static Map.
420         *
421         * @param addr
422         * @throws OGCWebServiceException
423         * @throws InvalidCapabilitiesException
424         */
425        private void loadWFSCapabilities( String addr )
426                                throws OGCWebServiceException, InvalidCapabilitiesException {
427    
428            LOG.logDebug( "reading capabilities from: ", addr );
429            WFSCapabilitiesDocument doc = new WFSCapabilitiesDocument();
430            try {
431                doc.load( new URL( OWSUtils.validateHTTPGetBaseURL( addr )
432                                   + "version=1.1.0&service=WFS&request=GetCapabilities" ) );
433            } catch ( Exception e ) {
434                LOG.logInfo( OWSUtils.validateHTTPGetBaseURL( addr ) + "version=1.1.0&service=WFS&request=GetCapabilities" );
435                LOG.logError( e.getMessage(), e );
436                throw new OGCWebServiceException( "could not read capabilities from WFS: " + addr );
437            }
438            WFSCapabilities capa = (WFSCapabilities) doc.parseCapabilities();
439            capaMap.put( addr, capa );
440        }
441    
442        /**
443         * creates a WFS GetFeature query from a named template and a set of KVP-encoded properties
444         *
445         * @param queryTemplate
446         * @param filterProps
447         * @return the query
448         * @throws PortalException
449         */
450        private String createQueryFromTemplate( String queryTemplate, RPCParameter[] filterProps )
451                                throws PortalException {
452    
453            queryTemplate = getInitParam( queryTemplate );
454            if ( !( new File( queryTemplate ).isAbsolute() ) ) {
455                queryTemplate = sc.getRealPath( queryTemplate );
456            }
457            StringBuffer template = new StringBuffer( 10000 );
458            try {
459                BufferedReader br = new BufferedReader( new FileReader( queryTemplate ) );
460                String line = null;
461                while ( ( line = br.readLine() ) != null ) {
462                    template.append( line );
463                }
464                br.close();
465            } catch ( IOException e ) {
466                LOG.logError( e.getMessage(), e );
467                throw new PortalException( "could not read query template: " + parameter.get( "TEMPLATE" ) );
468            }
469            String query = template.toString();
470            if ( filterProps != null ) {
471                for ( int i = 0; i < filterProps.length; i++ ) {
472                    RPCStruct struct = (RPCStruct) filterProps[i].getValue();
473                    String name = RPCUtils.getRpcPropertyAsString( struct, "propertyName" );
474                    String value = RPCUtils.getRpcPropertyAsString( struct, "value" );
475                    value = StringTools.replace( value, "XXX", "%", true );
476                    query = StringTools.replace( query, '$' + name, value, true );
477                }
478            }
479            return query;
480        }
481    
482        /**
483         * creates a WFS GetFeature query from a OGC filter expression send from a client
484         *
485         * @return the query
486         * @throws PortalException
487         */
488        private String createQueryFromFilter( String[] featureTypes, String[] xmlns, String filter ) {
489            StringBuffer query = new StringBuffer( 20000 );
490            String format = "text/xml; subtype=gml/3.1.1";
491            int maxFeatures = -1;
492            String resultType = "results";
493            if ( parameter.get( "OUTPUTFORMAT" ) != null ) {
494                format = parameter.get( "OUTPUTFORMAT" );
495            }
496            if ( parameter.get( "MAXFEATURE" ) != null ) {
497                maxFeatures = Integer.parseInt( parameter.get( "MAXFEATURE" ) );
498            }
499            if ( parameter.get( "RESULTTYPE" ) != null ) {
500                resultType = parameter.get( "RESULTTYPE" );
501            }
502            query.append( "<wfs:GetFeature outputFormat='" ).append( format );
503            query.append( "' maxFeatures='" ).append( maxFeatures ).append( "' " );
504            query.append( " resultType='" ).append( resultType ).append( "' " );
505            for ( int i = 0; i < xmlns.length; i++ ) {
506                String[] tmp = StringTools.toArray( xmlns[i], "=", false );
507                query.append( "xmlns:" ).append( tmp[0] ).append( "='" );
508                query.append( tmp[1] ).append( "' " );
509            }
510            query.append( "xmlns:wfs='http://www.opengis.net/wfs' " );
511            query.append( "xmlns:ogc='http://www.opengis.net/ogc' " );
512            query.append( "xmlns:gml='http://www.opengis.net/gml' " );
513            query.append( ">" );
514    
515            query.append( "<wfs:Query " );
516            for ( int i = 0; i < featureTypes.length; i++ ) {
517                query.append( "typeName='" ).append( featureTypes[i] );
518                if ( i < featureTypes.length - 1 ) {
519                    query.append( "," );
520                }
521            }
522            query.append( "'>" );
523            query.append( filter );
524            query.append( "</wfs:Query></wfs:GetFeature>" );
525    
526            return query.toString();
527        }
528    
529        /**
530         * creates an OGC FE filter from a set of KVP-encode properties and logical opertaions
531         *
532         * @return the filter
533         */
534        private String createFilterFromProperties() {
535            String tmp = parameter.get( "FILTERPROPERTIES" );
536            if ( tmp != null ) {
537                String[] properties = StringTools.extractStrings( tmp, "{", "}" );
538                String logOp = parameter.get( "LOGICALOPERATOR" );
539                StringBuffer filter = new StringBuffer( 10000 );
540                filter.append( "<ogc:Filter>" );
541                if ( properties.length > 1 ) {
542                    filter.append( "<ogc:" ).append( logOp ).append( '>' );
543                }
544                for ( int i = 0; i < properties.length; i++ ) {
545                    String[] prop = StringTools.extractStrings( tmp, "[", "]" );
546                    if ( "!=".equals( prop[1] ) || "NOT LIKE".equals( prop[1] ) ) {
547                        filter.append( "<ogc:Not>" );
548                    }
549                    if ( "=".equals( prop[1] ) || "!=".equals( prop[1] ) ) {
550                        filter.append( "<ogc:PropertyIsEqualTo>" );
551                        filter.append( "<ogc:PropertyName>" ).append( prop[0] ).append( "</ogc:PropertyName>" );
552                        filter.append( "<ogc:Literal>" ).append( prop[2] ).append( "</ogc:Literal>" );
553                        filter.append( "</ogc:PropertyIsEqualTo>" );
554                    } else if ( ">=".equals( prop[1] ) ) {
555                        filter.append( "<ogc:PropertyIsGreaterThanOrEqualTo>" );
556                        filter.append( "<ogc:PropertyName>" ).append( prop[0] ).append( "</ogc:PropertyName>" );
557                        filter.append( "<ogc:Literal>" ).append( prop[2] ).append( "</ogc:Literal>" );
558                        filter.append( "</ogc:PropertyIsGreaterThanOrEqualTo>" );
559                    } else if ( ">".equals( prop[1] ) ) {
560                        filter.append( "<ogc:PropertyIsGreaterThan>" );
561                        filter.append( "<ogc:PropertyName>" ).append( prop[0] ).append( "</ogc:PropertyName>" );
562                        filter.append( "<ogc:Literal>" ).append( prop[2] ).append( "</ogc:Literal>" );
563                        filter.append( "</ogc:PropertyIsGreaterThan>" );
564                    } else if ( "<=".equals( prop[1] ) ) {
565                        filter.append( "<ogc:PropertyIsLessThanOrEqualTo>" );
566                        filter.append( "<ogc:PropertyName>" ).append( prop[0] ).append( "</ogc:PropertyName>" );
567                        filter.append( "<ogc:Literal>" ).append( prop[2] ).append( "</ogc:Literal>" );
568                        filter.append( "</ogc:PropertyIsLessThanOrEqualTo>" );
569                    } else if ( "<".equals( prop[1] ) ) {
570                        filter.append( "<ogc:PropertyIsLessThan>" );
571                        filter.append( "<ogc:PropertyName>" ).append( prop[0] ).append( "</ogc:PropertyName>" );
572                        filter.append( "<ogc:Literal>" ).append( prop[2] ).append( "</ogc:Literal>" );
573                        filter.append( "</ogc:PropertyIsLessThan>" );
574                    } else if ( "LIKE".equals( prop[1] ) || "NOT LIKE".equals( prop[1] ) ) {
575                        filter.append( "<ogc:PropertyIsLike wildCard='%' singleChar='#' escape='!'>" );
576                        filter.append( "<ogc:PropertyName>" ).append( prop[0] ).append( "</ogc:PropertyName>" );
577                        filter.append( "<ogc:Literal>" ).append( prop[2] ).append( "</ogc:Literal>" );
578                        filter.append( "</ogc:PropertyIsLike>" );
579                    }
580                    if ( "!=".equals( prop[1] ) || "NOT LIKE".equals( prop[1] ) ) {
581                        filter.append( "</ogc:Not>" );
582                    }
583                }
584                if ( properties.length > 1 ) {
585                    filter.append( "</ogc:" ).append( logOp ).append( '>' );
586                }
587                filter.append( "</ogc:Filter>" );
588                return filter.toString();
589            }
590            return "";
591        }
592    
593    }