001 //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.4_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 }