001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_testing/src/org/deegree/enterprise/servlet/WFSHandler.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    
037    package org.deegree.enterprise.servlet;
038    
039    import static org.deegree.framework.util.CharsetUtils.getSystemCharset;
040    import static org.deegree.owscommon.XMLFactory.exportExceptionReportWFS;
041    import static org.deegree.owscommon.XMLFactory.exportExceptionReportWFS100;
042    
043    import java.io.IOException;
044    import java.io.ObjectOutputStream;
045    import java.io.OutputStream;
046    import java.net.MalformedURLException;
047    import java.net.URI;
048    import java.net.URL;
049    import java.util.HashMap;
050    import java.util.HashSet;
051    import java.util.Iterator;
052    import java.util.LinkedList;
053    import java.util.List;
054    import java.util.Map;
055    import java.util.Set;
056    
057    import javax.servlet.ServletOutputStream;
058    import javax.servlet.http.HttpServletResponse;
059    
060    import org.deegree.datatypes.QualifiedName;
061    import org.deegree.enterprise.ServiceException;
062    import org.deegree.framework.log.ILogger;
063    import org.deegree.framework.log.LoggerFactory;
064    import org.deegree.framework.util.CharsetUtils;
065    import org.deegree.framework.util.CollectionUtils;
066    import org.deegree.framework.util.CollectionUtils.Mapper;
067    import org.deegree.framework.xml.XMLFragment;
068    import org.deegree.framework.xml.XSLTDocument;
069    import org.deegree.i18n.Messages;
070    import org.deegree.io.datastore.schema.MappedFeatureType;
071    import org.deegree.io.datastore.schema.MappedGMLSchema;
072    import org.deegree.model.feature.FeatureCollection;
073    import org.deegree.model.feature.FeatureException;
074    import org.deegree.model.feature.FeatureTupleCollection;
075    import org.deegree.model.feature.GMLFeatureAdapter;
076    import org.deegree.model.feature.GMLFeatureCollectionDocument;
077    import org.deegree.model.spatialschema.GeometryException;
078    import org.deegree.ogcbase.PropertyPath;
079    import org.deegree.ogcwebservices.OGCWebServiceException;
080    import org.deegree.ogcwebservices.OGCWebServiceRequest;
081    import org.deegree.ogcwebservices.getcapabilities.DCPType;
082    import org.deegree.ogcwebservices.getcapabilities.HTTP;
083    import org.deegree.ogcwebservices.getcapabilities.Operation;
084    import org.deegree.ogcwebservices.wfs.WFService;
085    import org.deegree.ogcwebservices.wfs.WFServiceFactory;
086    import org.deegree.ogcwebservices.wfs.XMLFactory;
087    import org.deegree.ogcwebservices.wfs.XMLFactory_1_0_0;
088    import org.deegree.ogcwebservices.wfs.capabilities.FormatType;
089    import org.deegree.ogcwebservices.wfs.capabilities.WFSCapabilities;
090    import org.deegree.ogcwebservices.wfs.capabilities.WFSCapabilitiesDocument;
091    import org.deegree.ogcwebservices.wfs.capabilities.WFSFeatureType;
092    import org.deegree.ogcwebservices.wfs.capabilities.WFSOperationsMetadata;
093    import org.deegree.ogcwebservices.wfs.configuration.WFSConfiguration;
094    import org.deegree.ogcwebservices.wfs.operation.AbstractWFSRequest;
095    import org.deegree.ogcwebservices.wfs.operation.AugmentableGetFeature;
096    import org.deegree.ogcwebservices.wfs.operation.DescribeFeatureType;
097    import org.deegree.ogcwebservices.wfs.operation.FeatureResult;
098    import org.deegree.ogcwebservices.wfs.operation.FeatureTypeDescription;
099    import org.deegree.ogcwebservices.wfs.operation.GetFeature;
100    import org.deegree.ogcwebservices.wfs.operation.GetFeatureWithLock;
101    import org.deegree.ogcwebservices.wfs.operation.GetGmlObject;
102    import org.deegree.ogcwebservices.wfs.operation.GmlResult;
103    import org.deegree.ogcwebservices.wfs.operation.LockFeature;
104    import org.deegree.ogcwebservices.wfs.operation.LockFeatureResponse;
105    import org.deegree.ogcwebservices.wfs.operation.Query;
106    import org.deegree.ogcwebservices.wfs.operation.WFSGetCapabilities;
107    import org.deegree.ogcwebservices.wfs.operation.transaction.Delete;
108    import org.deegree.ogcwebservices.wfs.operation.transaction.Insert;
109    import org.deegree.ogcwebservices.wfs.operation.transaction.Transaction;
110    import org.deegree.ogcwebservices.wfs.operation.transaction.TransactionOperation;
111    import org.deegree.ogcwebservices.wfs.operation.transaction.TransactionResponse;
112    import org.deegree.ogcwebservices.wfs.operation.transaction.Update;
113    import org.w3c.dom.Document;
114    import org.w3c.dom.NamedNodeMap;
115    import org.w3c.dom.Node;
116    import org.w3c.dom.NodeList;
117    
118    /**
119     * Web servlet client for WFS.
120     * <p>
121     * NOTE: Currently, the <code>WFSHandler</code> is responsible for the pre- and postprocessing of virtual feature types.
122     * For virtual feature types, requests and responses are transformed using an XSL-script. Virtual feature types can also
123     * provide their own schema document that is sent as a response to {@link DescribeFeatureType} requests.
124     * <p>
125     * The heuristics that determines whether pre- or postprocessing is necessary, is not very accurate; check the methods:
126     * <ul>
127     * <li><code>#determineFormat(DescribeFeatureType, WFSConfiguration)</code></li>
128     * <li><code>#determineFormat(GetFeature, WFSConfiguration)</code></li>
129     * <li><code>#determineFormat(Transaction, WFSConfiguration)</code></li>
130     * </ul>
131     * <p>
132     * The code for the handling of virtual features should probably be moved to the {@link WFService} class.
133     * 
134     * @author <a href="mailto:poth@lat-lon.de">Andreas Poth </a>
135     * @author <a href="mailto:tfr@users.sourceforge.net">Torsten Friebe </a>
136     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a>
137     * @author last edited by: $Author: mschneider $
138     * 
139     * @version $Revision: 21632 $, $Date: 2009-12-23 10:31:16 +0100 (Mi, 23. Dez 2009) $
140     */
141    class WFSHandler extends AbstractOWServiceHandler {
142    
143        private static ILogger LOG = LoggerFactory.getLogger( WFSHandler.class );
144    
145        private static Map<URL, XSLTDocument> xsltCache;
146        static {
147            if ( xsltCache == null ) {
148                xsltCache = new HashMap<URL, XSLTDocument>();
149            }
150        }
151    
152        private URL getGmlObjectUrl;
153    
154        public WFSHandler() {
155            for ( Operation o : WFServiceFactory.getConfiguration().getOperationsMetadata().getOperations() ) {
156                if ( o.getName().equals( "GetGmlObject" ) ) {
157                    getGmlObjectUrl = ( (HTTP) o.getDCPs()[0].getProtocol() ).getGetOnlineResources()[0];
158                }
159            }
160        }
161    
162        /**
163         * Performs the given {@link OGCWebServiceRequest} on the {@link WFService} and sends the response to the given
164         * {@link HttpServletResponse} object.
165         * 
166         * @param request
167         *            OGCWebServiceRequest to be performed
168         * @param httpResponse
169         *            servlet response object to write to
170         * @throws ServiceException
171         */
172        public void perform( OGCWebServiceRequest request, HttpServletResponse httpResponse )
173                                throws ServiceException {
174    
175            LOG.logDebug( "Performing request: " + request.toString() );
176    
177            try {
178                WFService service = WFServiceFactory.createInstance();
179                if ( request instanceof WFSGetCapabilities ) {
180                    performGetCapabilities( service, (WFSGetCapabilities) request, httpResponse );
181                } else if ( request instanceof DescribeFeatureType ) {
182                    ( (DescribeFeatureType) request ).guessMissingNamespaces( service );
183                    performDescribeFeatureType( service, (DescribeFeatureType) request, httpResponse );
184                } else if ( request instanceof GetFeature ) {
185                    ( (GetFeature) request ).guessMissingTypeNameNamespaces( service );
186                    performGetFeature( service, (GetFeature) request, httpResponse );
187                } else if ( request instanceof Transaction ) {
188                    performTransaction( service, (Transaction) request, httpResponse );
189                } else if ( request instanceof LockFeature ) {
190                    ( (LockFeature) request ).guessMissingNamespaces( service );
191                    performLockFeature( service, (LockFeature) request, httpResponse );
192                } else if ( request instanceof GetGmlObject ) {
193                    performGetGmlObject( service, (GetGmlObject) request, httpResponse );
194                } else {
195                    assert false : "Unhandled WFS request type: '" + request.getClass().getName() + "'";
196                }
197            } catch ( OGCWebServiceException e ) {
198                LOG.logInfo( "Error while performing WFS request.", e );
199                sendVersionedException( httpResponse, e, "1.0.0".equals( request.getVersion() ) );
200            } catch ( Exception e ) {
201                LOG.logError( "Fatal error while performing WFS request.", e );
202                sendVersionedException( httpResponse, new OGCWebServiceException( getClass().getName(), e.getMessage() ),
203                                        "1.0.0".equals( request.getVersion() ) );
204            }
205        }
206    
207        /**
208         * Performs a {@link WFSGetCapabilities} request and sends the response to the given {@link HttpServletResponse}
209         * object.
210         * 
211         * @param service
212         *            WFService instance to be used
213         * @param request
214         *            GetCapabilities request to be performed
215         * @param httpResponse
216         *            servlet response object to write to
217         * @throws OGCWebServiceException
218         */
219        private void performGetCapabilities( WFService service, WFSGetCapabilities request, HttpServletResponse httpResponse )
220                                throws OGCWebServiceException {
221    
222            WFSCapabilities capa = (WFSCapabilities) service.doService( request );
223    
224            try {
225                httpResponse.setContentType( "application/xml" );
226                WFSCapabilitiesDocument document = null;
227                String version = request.getVersion();
228                boolean use_1_1_0 = true;
229                LOG.logDebug( "Version of incoming request is: " + version );
230                if ( "1.0.0".compareTo( version ) >= 0 ) {
231                    use_1_1_0 = false;
232                }
233                if ( !use_1_1_0 ) {
234                    document = XMLFactory_1_0_0.getInstance().export( (WFSConfiguration) capa );
235                } else {
236                    document = XMLFactory.export( capa, request.getSections() );
237                }
238                OutputStream os = httpResponse.getOutputStream();
239                document.write( os );
240                os.close();
241            } catch ( IOException e ) {
242                LOG.logError( "Error sending GetCapabilities response to client.", e );
243            }
244        }
245    
246        /**
247         * Performs a {@link DescribeFeatureType} request and sends the response to the given {@link HttpServletResponse}
248         * object.
249         * 
250         * @param service
251         *            WFService instance to be used
252         * @param request
253         *            DescribeFeatureType request to be performed
254         * @param httpResponse
255         *            servlet response object to write to
256         * @throws OGCWebServiceException
257         */
258        private void performDescribeFeatureType( WFService service, DescribeFeatureType request,
259                                                 HttpServletResponse httpResponse )
260                                throws OGCWebServiceException {
261    
262            WFSConfiguration config = (WFSConfiguration) service.getCapabilities();
263            FormatType format = determineFormat( request, config, service );
264    
265            XMLFragment schemaDoc = null;
266    
267            if ( format.getSchemaLocation() != null ) {
268    
269                // check for requested types <-> configured types
270                WFSFeatureType[] featureTypes = config.getFeatureTypeList().getFeatureTypes();
271                HashSet<String> set = new HashSet<String>( featureTypes.length );
272                set.addAll( CollectionUtils.map( featureTypes, new Mapper<String, WFSFeatureType>() {
273                    public String apply( WFSFeatureType u ) {
274                        return u.getName().getLocalName();
275                    }
276                } ) );
277                for ( QualifiedName name : request.getTypeNames() ) {
278                    if ( !set.contains( name.getLocalName() ) ) {
279                        String msg = Messages.getMessage( "WFS_FEATURE_TYPE_UNKNOWN", name.getLocalName() );
280                        throw new OGCWebServiceException( this.getClass().getName(), msg );
281                    }
282                }
283    
284                // read special schema for virtual format
285                try {
286                    schemaDoc = new XMLFragment( format.getSchemaLocation().toURL() );
287                } catch ( Exception e ) {
288                    String msg = Messages.getMessage( "WFS_VIRTUAL_FORMAT_SCHEMA_READ_ERROR", format.getSchemaLocation(),
289                                                      format.getValue(), e );
290                    LOG.logError( msg, e );
291                    throw new OGCWebServiceException( getClass().getName(), msg );
292                }
293            } else {
294                // get schema from WFService
295                FeatureTypeDescription ftDescription = (FeatureTypeDescription) service.doService( request );
296                schemaDoc = ftDescription.getFeatureTypeSchema();
297            }
298    
299            httpResponse.setContentType( "text/xml; charset=" + CharsetUtils.getSystemCharset() );
300            try {
301                schemaDoc.write( httpResponse.getOutputStream() );
302            } catch ( IOException e ) {
303                LOG.logError( "Error sending DescribeFeatureType response to client.", e );
304            }
305        }
306    
307        /**
308         * Performs a {@link GetFeature} request and sends the response to the given {@link HttpServletResponse} object.
309         * 
310         * @param service
311         *            WFService instance to be used
312         * @param request
313         *            GetFeature request to be performed
314         * @param httpResponse
315         *            servlet response object to write to
316         * @throws OGCWebServiceException
317         */
318        private void performGetFeature( WFService service, GetFeature request, HttpServletResponse httpResponse )
319                                throws OGCWebServiceException {
320    
321            // hack: augment request if it was a KVP request with FEATUREID and no TYPENAME
322            if ( request instanceof AugmentableGetFeature ) {
323                ( (AugmentableGetFeature) request ).augment( (WFSConfiguration) service.getCapabilities() );
324            }
325    
326            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
327                try {
328                    XMLFragment xml = XMLFactory.export( request );
329                    LOG.logDebug( xml.getAsPrettyString() );
330                } catch ( Exception e ) {
331                    // nothing to do
332                }
333            }
334    
335            WFSConfiguration config = (WFSConfiguration) service.getCapabilities();
336            FormatType formatType = determineFormat( request, config, service );
337    
338            // perform pre-processing if necessary (XSLT)
339            if ( formatType.isVirtual() ) {
340                request = transformGetFeature( request, formatType );
341            }
342    
343            request.guessAllMissingNamespaces( service );
344    
345            // perform request on WFService
346            FeatureResult result = (FeatureResult) service.doService( request );
347            FeatureCollection fc = (FeatureCollection) result.getResponse();
348    
349            String format = formatType.getValue();
350    
351            if ( GetFeature.FORMAT_FEATURECOLLECTION.equals( format ) ) {
352                sendBinaryResponse( fc, httpResponse );
353            } else if ( AbstractWFSRequest.FORMAT_GML2_WFS100.equals( format )
354                        || AbstractWFSRequest.FORMAT_XML.equals( format ) || format.startsWith( "text/xml; subtype=" ) ) {
355                String schemaURL = buildSchemaURL( service, request );
356                boolean suppressXLink = suppressXLinkOutput( fc );
357                int depth = request.getTraverseXLinkDepth();
358                if ( formatType.getOutFilter() != null ) {
359                    sendTransformedResponse( fc, httpResponse, schemaURL, suppressXLink, formatType,
360                                             config.getDeegreeParams().printGeometryGmlIds(), depth, request );
361                } else {
362                    sendGMLResponse( fc, httpResponse, schemaURL, suppressXLink,
363                                     config.getDeegreeParams().printGeometryGmlIds(), depth, request );
364                }
365            } else {
366                String msg = Messages.getMessage( "WFS_QUERY_UNSUPPORTED_FORMAT2", format );
367                throw new OGCWebServiceException( msg );
368            }
369        }
370    
371        private void performGetGmlObject( WFService service, GetGmlObject request, HttpServletResponse httpResponse )
372                                throws OGCWebServiceException, IOException, FeatureException, GeometryException {
373            GmlResult result = (GmlResult) service.doService( request );
374            OGCWebServiceException exception = result.getException();
375            if ( exception != null ) {
376                throw exception;
377            }
378            ServletOutputStream out = httpResponse.getOutputStream();
379            result.writeResult( out );
380            out.close();
381        }
382    
383        /**
384         * Performs a {@link LockFeature} request and sends the response to the given {@link HttpServletResponse} object.
385         * 
386         * @param service
387         *            WFService instance to be used
388         * @param request
389         *            LockFeature request to be performed
390         * @param httpResponse
391         *            servlet response object to write to
392         * @throws OGCWebServiceException
393         */
394        private void performLockFeature( WFService service, LockFeature request, HttpServletResponse httpResponse )
395                                throws OGCWebServiceException {
396    
397            LockFeatureResponse response = (LockFeatureResponse) service.doService( request );
398            XMLFragment responseDoc;
399            try {
400                if ( request.getVersion().equals( "1.0.0" ) ) {
401                    responseDoc = XMLFactory_1_0_0.export( response );
402                } else {
403                    responseDoc = XMLFactory.export( response );
404                }
405            } catch ( Exception e ) {
406                LOG.logError( "Unknown error", e );
407                throw new OGCWebServiceException( this.getClass().getName(), e.getMessage() );
408            }
409    
410            httpResponse.setContentType( "text/xml; charset=" + CharsetUtils.getSystemCharset() );
411            try {
412                responseDoc.write( httpResponse.getOutputStream() );
413            } catch ( IOException e ) {
414                LOG.logError( "Error sending LockFeature response to client.", e );
415            }
416        }
417    
418        /**
419         * Builds a KVP-encoded DescribeFeatureType-request that can be used to fetch the schemas for all feature types are
420         * that queried in the given {@link GetFeature} request.
421         * 
422         * @param service
423         * @param request
424         * @return KVP-encoded DescribeFeatureType-request
425         */
426        private String buildSchemaURL( WFService service, GetFeature request ) {
427    
428            String schemaURL = null;
429    
430            WFSCapabilities capa = service.getCapabilities();
431            WFSOperationsMetadata opMetadata = (WFSOperationsMetadata) capa.getOperationsMetadata();
432            Operation describeFTOperation = opMetadata.getDescribeFeatureType();
433            DCPType[] dcpTypes = describeFTOperation.getDCPs();
434            if ( dcpTypes.length > 0 && dcpTypes[0].getProtocol() instanceof HTTP ) {
435                HTTP http = (HTTP) dcpTypes[0].getProtocol();
436                if ( http.getGetOnlineResources().length > 0 ) {
437                    URL baseURL = http.getGetOnlineResources()[0];
438                    String requestPart = buildDescribeFTRequest( request );
439                    schemaURL = baseURL.toString() + requestPart;
440                }
441            }
442            return schemaURL;
443        }
444    
445        /**
446         * Builds the parameter part for a KVP-encoded DescribeFeatureType-request that fetches the necessary schemas for
447         * all feature types that are queried in the given {@link GetFeature} request.
448         * 
449         * @param request
450         * @return the URL-encoded parameter part of a KVP-DescribeFeatureType request
451         */
452        private String buildDescribeFTRequest( GetFeature request ) {
453    
454            Set<QualifiedName> ftNames = new HashSet<QualifiedName>();
455            Map<String, URI> nsBindings = new HashMap<String, URI>();
456    
457            // get all requested feature types
458            Query[] queries = request.getQuery();
459            for ( Query query : queries ) {
460                QualifiedName[] typeNames = query.getTypeNames();
461                for ( QualifiedName name : typeNames ) {
462                    ftNames.add( name );
463                }
464            }
465            Iterator<QualifiedName> ftNameIter = ftNames.iterator();
466            QualifiedName qn = ftNameIter.next();
467            StringBuffer typeNameSb = new StringBuffer( qn.getPrefix() );
468            typeNameSb.append( ':' ).append( qn.getLocalName() );
469            while ( ftNameIter.hasNext() ) {
470                typeNameSb.append( ',' );
471                qn = ftNameIter.next();
472                typeNameSb.append( qn.getPrefix() );
473                typeNameSb.append( ':' ).append( qn.getLocalName() );
474            }
475    
476            // get all used namespace bindings
477            for ( QualifiedName ftName : ftNames ) {
478                LOG.logDebug( "for featuretype: " + ftName.getLocalName() + " found namespace binding: "
479                              + ftName.getNamespace() );
480                nsBindings.put( ftName.getPrefix(), ftName.getNamespace() );
481            }
482            StringBuffer nsParamSb = new StringBuffer( "xmlns(" );
483            Iterator<String> prefixIter = nsBindings.keySet().iterator();
484            String prefix = prefixIter.next();
485            nsParamSb.append( prefix );
486            nsParamSb.append( '=' );
487            nsParamSb.append( nsBindings.get( prefix ) );
488            while ( prefixIter.hasNext() ) {
489                nsParamSb.append( ',' );
490                prefix = prefixIter.next();
491                nsParamSb.append( prefix );
492                nsParamSb.append( '=' );
493                nsParamSb.append( nsBindings.get( prefix ) );
494            }
495            nsParamSb.append( ')' );
496    
497            // build KVP-DescribeFeatureType-request
498            StringBuffer sb = new StringBuffer( "SERVICE=WFS" );
499            sb.append( "&VERSION=" + request.getVersion() );
500            // sb.append( "&VERSION=1.1.0" );
501            sb.append( "&REQUEST=DescribeFeatureType" );
502    
503            // append TYPENAME parameter
504            sb.append( "&TYPENAME=" );
505            sb.append( typeNameSb );
506    
507            // append NAMESPACE parameter
508            sb.append( "&NAMESPACE=" );
509            sb.append( nsParamSb.toString() );
510    
511            return sb.toString();
512        }
513    
514        /**
515         * Transforms a {@link GetFeature} request depending on the requested virtual format.
516         * 
517         * @param request
518         *            GetFeature request to be transformed
519         * @param format
520         *            requested (virtual) output format
521         * @return transformed GetFeature requested
522         * @throws OGCWebServiceException
523         *             if transformation script could not be loaded or transformation failed
524         */
525        private GetFeature transformGetFeature( GetFeature request, FormatType format )
526                                throws OGCWebServiceException {
527    
528            if ( request instanceof GetFeatureWithLock ) {
529                LOG.logDebug( "Not transforming GetFeatureWithLock request, it's not supported yet." );
530                return request;
531            }
532    
533            LOG.logDebug( "Transforming GetFeature request." );
534            long start = System.currentTimeMillis();
535    
536            URL inFilterURL = null;
537            try {
538                inFilterURL = format.getInFilter().toURL();
539            } catch ( MalformedURLException e1 ) {
540                // never happens
541            }
542            XSLTDocument xsl = xsltCache.get( inFilterURL );
543            if ( xsl == null ) {
544                xsl = new XSLTDocument();
545                try {
546                    xsl.load( inFilterURL );
547                    xsltCache.put( inFilterURL, xsl );
548                } catch ( Exception e ) {
549                    String msg = Messages.getMessage( "WFS_PREPROCESS_XSL_FILE_ERROR", format.getValue(),
550                                                      format.getInFilter().toString(), e );
551                    LOG.logError( msg, e );
552                    throw new OGCWebServiceException( getClass().getName(), msg );
553                }
554            }
555    
556            XMLFragment xml = null;
557            try {
558                xml = XMLFactory.export( request );
559                // nasty workaround to allow the Java methods called by XSL to access the namespace bindings
560                String nsp = getAllNamespaceDeclarations( xml.getRootElement().getOwnerDocument() );
561                Map<String, String> params = new HashMap<String, String>();
562                params.put( "NSP", nsp );
563                LOG.logDebug( "Namespace string given to XSL: " + nsp );
564                xml = xsl.transform( xml, format.getInFilter().toASCIIString(), null, params );
565            } catch ( Exception e ) {
566                String msg = Messages.getMessage( "WFS_PREPROCESS_XSL_ERROR", format.getValue(), e );
567                LOG.logError( msg, e );
568                throw new OGCWebServiceException( getClass().getName(), msg );
569            }
570    
571            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
572                LOG.logDebug( "Successfully transformed GetFeature request in " + ( System.currentTimeMillis() - start )
573                              + " milliseconds." );
574                try {
575                    LOG.logDebugXMLFile( "WFSHandler_GetFeature_transformed", xml );
576                } catch ( Exception e ) {
577                    LOG.logError( e.getMessage(), e );
578                }
579            }
580            return GetFeature.create( request.getId(), xml.getRootElement() );
581        }
582    
583        /**
584         * Sends the given {@link FeatureCollection} as GML to the given {@link HttpServletResponse} object.
585         * 
586         * @param fc
587         *            feature collection to send
588         * @param httpResponse
589         *            servlet response object to write to
590         * @param schemaURL
591         *            URL to schema document (DescribeFeatureType request)
592         * @param suppressXLinks
593         *            true, if no XLinks must be used in the output, false otherwise
594         * @param depth
595         *            the depth of xlinks to resolve
596         */
597        private void sendGMLResponse( FeatureCollection fc, HttpServletResponse httpResponse, String schemaURL,
598                                      boolean suppressXLinks, boolean sendGeometryIds, int depth, GetFeature request ) {
599    
600            try {
601                httpResponse.setContentType( "text/xml; charset=" + CharsetUtils.getSystemCharset() );
602                OutputStream os = httpResponse.getOutputStream();
603                GMLFeatureAdapter featureAdapter = new GMLFeatureAdapter( suppressXLinks, schemaURL, sendGeometryIds, depth );
604                List<PropertyPath[]> names = new LinkedList<PropertyPath[]>();
605                for ( Query q : request.getQuery() ) {
606                    names.add( q.getPropertyNames() );
607                }
608                featureAdapter.setPropertyPaths( names );
609                if ( getGmlObjectUrl != null ) {
610                    featureAdapter.setBaseURL( getGmlObjectUrl.toExternalForm() );
611                }
612                featureAdapter.export( fc, os, getSystemCharset() );
613            } catch ( Exception e ) {
614                LOG.logError( "Error sending GetFeature response (GML) to client.", e );
615            }
616        }
617    
618        /**
619         * Sends the given {@link FeatureCollection} as a serialized Java object to the given {@link HttpServletResponse}
620         * object.
621         * 
622         * @param fc
623         *            feature collection to send
624         * @param httpResponse
625         *            servlet response object to write to
626         */
627        private void sendBinaryResponse( FeatureCollection fc, HttpServletResponse httpResponse ) {
628            try {
629                OutputStream os = httpResponse.getOutputStream();
630                ObjectOutputStream oos = new ObjectOutputStream( os );
631                oos.writeObject( fc );
632                oos.flush();
633            } catch ( IOException e ) {
634                LOG.logError( "Error sending GetFeature response (binary) to client.", e );
635            }
636        }
637    
638        /**
639         * Transforms a {@link FeatureCollection} to the given format using XSLT and sends it to the specified
640         * {@link HttpServletResponse} object.
641         * 
642         * @param fc
643         *            feature collection to send
644         * @param schemaURL
645         *            URL to schema document (DescribeFeatureType request)
646         * @param httpResponse
647         *            servlet response object to write to
648         * @param suppressXLinks
649         *            true, if no XLinks must be used in the output, false otherwise
650         * @param format
651         *            requested format
652         * @param sendGeometryIds
653         *            whether to send geometry gml ids
654         * @param depth
655         *            the xlink depth to resolve
656         */
657        private void sendTransformedResponse( FeatureCollection fc, HttpServletResponse httpResponse, String schemaURL,
658                                              boolean suppressXLinks, FormatType format, boolean sendGeometryIds,
659                                              int depth, GetFeature request )
660                                throws OGCWebServiceException {
661    
662            GMLFeatureCollectionDocument fcgml = null;
663            try {
664                // export result feature collection as GML to enable transformation
665                // into another (XML) format
666                GMLFeatureAdapter featureAdapter = null;
667                if ( "GML2".equals( format.getValue() ) ) {
668                    String wfsSchemaBinding = "http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.0.0/WFS-basic.xsd";
669                    featureAdapter = new GMLFeatureAdapter( suppressXLinks, schemaURL, wfsSchemaBinding, sendGeometryIds,
670                                                            depth );
671                } else {
672                    featureAdapter = new GMLFeatureAdapter( suppressXLinks, schemaURL, sendGeometryIds, depth );
673                }
674                List<PropertyPath[]> names = new LinkedList<PropertyPath[]>();
675                for ( Query q : request.getQuery() ) {
676                    names.add( q.getPropertyNames() );
677                }
678                featureAdapter.setPropertyPaths( names );
679                fcgml = featureAdapter.export( fc );
680            } catch ( Exception e ) {
681                String msg = "Could not export feature collection to GML: " + e.getMessage();
682                LOG.logError( msg, e );
683                throw new OGCWebServiceException( msg );
684            }
685    
686            LOG.logDebug( "Transforming GetFeature response." );
687            long start = System.currentTimeMillis();
688    
689            // TODO: cache Transformer
690            XSLTDocument xsl = null;
691            try {
692                xsl = new XSLTDocument( format.getOutFilter().toURL() );
693            } catch ( Exception e ) {
694                String msg = Messages.getMessage( "WFS_POSTPROCESS_XSL_FILE_ERROR", format.getValue(),
695                                                  format.getOutFilter().toString(), e );
696                LOG.logError( msg );
697                throw new OGCWebServiceException( msg );
698            }
699    
700            try {
701                if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
702                    LOG.logDebugFile( "WFSHandler_GetFeature_result", ".xml", fcgml.getAsString() );
703                }
704    
705                String type = format.getValue().split( ";" )[0];
706                httpResponse.setContentType( type + "; charset=" + CharsetUtils.getSystemCharset() );
707    
708                OutputStream os = httpResponse.getOutputStream();
709                xsl.transform( fcgml, os );
710                os.close();
711            } catch ( Exception e ) {
712                String msg = Messages.getMessage( "WFS_POSTPROCESS_XSL_ERROR", format.getValue(), e );
713                LOG.logError( msg, e );
714                throw new OGCWebServiceException( getClass().getName(), msg );
715            }
716    
717            LOG.logDebug( "Successfully transformed GetFeature response in " + ( System.currentTimeMillis() - start )
718                          + " milliseconds." );
719        }
720    
721        /**
722         * Performs a {@link Transaction} request and sends the response to the given {@link HttpServletResponse} object.
723         * 
724         * @param service
725         *            WFService instance to be used
726         * @param request
727         *            Transaction request to be performed
728         * @param httpResponse
729         *            servlet response object to write to
730         * @throws OGCWebServiceException
731         */
732        private void performTransaction( WFService service, Transaction request, HttpServletResponse httpResponse )
733                                throws OGCWebServiceException {
734    
735            WFSConfiguration config = (WFSConfiguration) service.getCapabilities();
736            FormatType format = determineFormat( request, config );
737    
738            // perform pre-processing if necessary (XSLT)
739            if ( format.isVirtual() ) {
740                request = transformTransaction( request, format );
741            }
742    
743            TransactionResponse response = (TransactionResponse) service.doService( request );
744    
745            try {
746                httpResponse.setContentType( "text/xml; charset=" + CharsetUtils.getSystemCharset() );
747                XMLFragment document;
748                if ( request.getVersion().equals( "1.0.0" ) ) {
749                    document = XMLFactory_1_0_0.export( response );
750                } else {
751                    document = XMLFactory.export( response );
752                }
753                document.write( httpResponse.getOutputStream() );
754            } catch ( IOException e ) {
755                LOG.logError( "Error sending Transaction response to client.", e );
756            }
757        }
758    
759        /**
760         * Transforms a {@link Transaction} request depending on the requested virtual format.
761         * 
762         * @param request
763         *            Transaction request to be transformed
764         * @param format
765         *            requested (virtual) output format
766         * @return transformed Transaction
767         * @throws OGCWebServiceException
768         *             if transformation script could not be loaded or transformation failed
769         */
770        private Transaction transformTransaction( Transaction request, FormatType format )
771                                throws OGCWebServiceException {
772    
773            LOG.logDebug( "Transforming Transaction request." );
774            long start = System.currentTimeMillis();
775    
776            URL inFilterURL = null;
777            try {
778                inFilterURL = format.getInFilter().toURL();
779            } catch ( MalformedURLException e1 ) {
780                // never happens
781            }
782            XSLTDocument xsl = xsltCache.get( inFilterURL );
783            if ( xsl == null ) {
784                xsl = new XSLTDocument();
785                try {
786                    LOG.logDebug( "Read Filter ... " );
787                    xsl.load( inFilterURL );
788                    xsltCache.put( inFilterURL, xsl );
789                } catch ( Exception e ) {
790                    String msg = Messages.getMessage( "WFS_PREPROCESS_XSL_FILE_ERROR", format.getValue(),
791                                                      format.getInFilter().toString(), e );
792                    LOG.logError( msg, e );
793                    throw new OGCWebServiceException( getClass().getName(), msg );
794                }
795            }
796    
797            XMLFragment xml = null;
798            try {
799                xml = request.getSourceDocument();
800                if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
801                    try {
802                        LOG.logDebugXMLFile( "WFSHandler_Transaction_incoming", xml );
803                    } catch ( Exception e ) {
804                        LOG.logError( e.getMessage(), e );
805                    }
806                }
807            } catch ( Exception e ) {
808                LOG.logError( e.getMessage(), e );
809                throw new OGCWebServiceException( getClass().getName(), e.getMessage() );
810            }
811            // transform Transaction request
812            try {
813                LOG.logDebug( "start transform ..." );
814                xml = xsl.transform( xml, format.getInFilter().toASCIIString(), null, null );
815                LOG.logDebug( "end transform ..." );
816                if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
817                    try {
818                        LOG.logDebugXMLFile( "WFSHandler_Transaction_transformed", xml );
819                    } catch ( Exception e ) {
820                        LOG.logError( e.getMessage(), e );
821                    }
822                }
823            } catch ( Exception e ) {
824                String msg = Messages.getMessage( "WFS_PREPROCESS_XSL_ERROR", format.getInFilter().toString(), e );
825                LOG.logError( msg, e );
826                throw new OGCWebServiceException( getClass().getName(), msg );
827            }
828    
829            try {
830                request = Transaction.create( request.getId(), xml.getRootElement() );
831            } catch ( Exception e ) {
832                LOG.logError( e.getMessage(), e );
833                throw new OGCWebServiceException( getClass().getName(), e.getMessage() );
834            }
835    
836            LOG.logDebug( "Successfully transformed Transaction request in " + ( System.currentTimeMillis() - start )
837                          + " milliseconds." );
838    
839            return request;
840        }
841    
842        /**
843         * Determines whether the response to the given {@link GetFeature} request may use XLinks or not.
844         * <p>
845         * The first feature of the collection is checked; if it's {@link MappedGMLSchema} requests the suppression of
846         * XLinks, xlinks are disabled, otherwise they are enabled.
847         * 
848         * @param fc
849         * @return true, if the response document must not contain XLinks, false otherwise
850         */
851        private boolean suppressXLinkOutput( FeatureCollection fc ) {
852    
853            boolean suppressXLinkOutput = false;
854    
855            if ( fc instanceof FeatureTupleCollection ) {
856                suppressXLinkOutput = true;
857            } else if ( fc.size() > 0 ) {
858                if ( fc.getFeature( 0 ).getFeatureType() instanceof MappedFeatureType ) {
859                    suppressXLinkOutput = ( (MappedFeatureType) fc.getFeature( 0 ).getFeatureType() ).getGMLSchema().suppressXLinkOutput();
860                }
861            }
862            return suppressXLinkOutput;
863        }
864    
865        private FormatType determineFormat( GetFeature request, WFSConfiguration config, WFService service )
866                                throws OGCWebServiceException {
867    
868            Query firstQuery = request.getQuery()[0];
869            QualifiedName ftName = firstQuery.getTypeNames()[0];
870            WFSFeatureType wfsFT = config.getFeatureTypeList().getFeatureType( ftName );
871            if ( wfsFT == null ) {
872                MappedFeatureType ft = service.getMappedFeatureType( ftName );
873                String msg = null;
874                if ( ft == null ) {
875                    msg = Messages.getMessage( "WFS_FEATURE_TYPE_UNKNOWN", ftName );
876                } else {
877                    assert !ft.isVisible();
878                    msg = Messages.getMessage( "WFS_FEATURE_TYPE_INVISIBLE", ftName );
879                }
880                throw new OGCWebServiceException( getClass().getName(), msg );
881            }
882            String requestedFormat = request.getOutputFormat();
883            FormatType format = wfsFT.getOutputFormat( requestedFormat );
884            if ( format == null ) {
885                String msg = Messages.getMessage( "WFS_QUERY_UNSUPPORTED_FORMAT", requestedFormat, ftName );
886                throw new OGCWebServiceException( getClass().getName(), msg );
887            }
888            return format;
889        }
890    
891        private FormatType determineFormat( DescribeFeatureType request, WFSConfiguration config, WFService service )
892                                throws OGCWebServiceException {
893    
894            // NOTE: this cannot cope with a mix of virtual and real features
895            QualifiedName ftName = null;
896            if ( request.getTypeNames().length > 0 ) {
897                ftName = request.getTypeNames()[0];
898            } else {
899                // use the first ft that is available in the requested format
900                for ( WFSFeatureType tp : config.getFeatureTypeList().getFeatureTypes() ) {
901                    if ( tp.getOutputFormat( request.getOutputFormat() ) != null ) {
902                        ftName = tp.getName();
903                        break;
904                    }
905                }
906            }
907            LOG.logDebug( "typeName: " + ftName );
908    
909            WFSFeatureType wfsFT = config.getFeatureTypeList().getFeatureType( ftName );
910            if ( wfsFT == null ) {
911                MappedFeatureType ft = service.getMappedFeatureType( ftName );
912                String msg = null;
913                if ( ft == null ) {
914                    msg = Messages.getMessage( "WFS_FEATURE_TYPE_UNKNOWN", ftName );
915                } else {
916                    assert !ft.isVisible();
917                    msg = Messages.getMessage( "WFS_FEATURE_TYPE_INVISIBLE", ftName );
918                }
919                throw new OGCWebServiceException( getClass().getName(), msg );
920            }
921            String requestedFormat = request.getOutputFormat();
922            LOG.logDebug( "requested outputformat: " + requestedFormat );
923            FormatType format = wfsFT.getOutputFormat( requestedFormat );
924            if ( format == null ) {
925                String msg = Messages.getMessage( "WFS_QUERY_UNSUPPORTED_FORMAT", requestedFormat, ftName );
926                throw new OGCWebServiceException( getClass().getName(), msg );
927            }
928            return format;
929        }
930    
931        private FormatType determineFormat( Transaction request, WFSConfiguration config )
932                                throws OGCWebServiceException {
933    
934            FormatType format = null;
935    
936            WFSFeatureType wfsFT = config.getFeatureTypeList().getFeatureTypes()[0];
937    
938            List<TransactionOperation> list = request.getOperations();
939            TransactionOperation op = list.get( 0 );
940            if ( op instanceof Insert ) {
941                QualifiedName qn = ( (Insert) op ).getAffectedFeatureTypes().get( 0 );
942                wfsFT = config.getFeatureTypeList().getFeatureType( qn );
943                if ( wfsFT == null ) {
944                    throw new OGCWebServiceException( Messages.getMessage( "WFS_INSERT_UNSUPPORTED_FT", qn ) );
945                }
946            } else if ( op instanceof Update ) {
947                QualifiedName qn = ( (Update) op ).getAffectedFeatureTypes().get( 0 );
948                wfsFT = config.getFeatureTypeList().getFeatureType( qn );
949                if ( wfsFT == null ) {
950                    throw new OGCWebServiceException( Messages.getMessage( "WFS_UPDATE_UNSUPPORTED_FT", qn ) );
951                }
952            } else if ( op instanceof Delete ) {
953                QualifiedName qn = ( (Delete) op ).getAffectedFeatureTypes().get( 0 );
954                wfsFT = config.getFeatureTypeList().getFeatureType( qn );
955                if ( wfsFT == null ) {
956                    throw new OGCWebServiceException( Messages.getMessage( "WFS_DELETE_UNSUPPORTED_FT", qn ) );
957                }
958            }
959    
960            FormatType[] formats = wfsFT.getOutputFormats();
961            for ( int i = 0; i < formats.length; i++ ) {
962                format = formats[i];
963                if ( format.getInFilter() != null ) {
964                    break;
965                }
966            }
967            return format;
968        }
969    
970        /**
971         * @param httpResponse
972         * @param serviceException
973         * @param is100
974         */
975        public void sendVersionedException( HttpServletResponse httpResponse, OGCWebServiceException serviceException,
976                                            boolean is100 ) {
977            try {
978                httpResponse.setContentType( "text/xml" );
979                XMLFragment reportDocument = is100 ? exportExceptionReportWFS100( serviceException )
980                                                  : exportExceptionReportWFS( serviceException );
981                OutputStream os = httpResponse.getOutputStream();
982                reportDocument.write( os );
983                os.close();
984            } catch ( Exception e ) {
985                LOG.logError( "Error sending exception report: ", e );
986            }
987    
988        }
989    
990        @Override
991        public void sendException( HttpServletResponse httpResponse, OGCWebServiceException serviceException ) {
992            try {
993                httpResponse.setContentType( "text/xml" );
994                XMLFragment reportDocument = exportExceptionReportWFS( serviceException );
995                OutputStream os = httpResponse.getOutputStream();
996                reportDocument.write( os );
997                os.close();
998            } catch ( Exception e ) {
999                LOG.logError( "Error sending exception report: ", e );
1000            }
1001    
1002        }
1003    
1004        private String getAllNamespaceDeclarations( Document doc ) {
1005            Map<String, String> nsp = new HashMap<String, String>();
1006            nsp = collect( nsp, doc );
1007    
1008            Iterator<String> iter = nsp.keySet().iterator();
1009            StringBuffer sb = new StringBuffer( 1000 );
1010            while ( iter.hasNext() ) {
1011                String s = iter.next();
1012                String val = nsp.get( s );
1013                sb.append( s ).append( ":" ).append( val );
1014                if ( iter.hasNext() ) {
1015                    sb.append( ';' );
1016                }
1017            }
1018            return sb.toString();
1019        }
1020    
1021        private Map<String, String> collect( Map<String, String> nsp, Node node ) {
1022            NamedNodeMap nnm = node.getAttributes();
1023            if ( nnm != null ) {
1024                for ( int i = 0; i < nnm.getLength(); i++ ) {
1025                    String s = nnm.item( i ).getNodeName();
1026                    if ( s.startsWith( "xmlns:" ) ) {
1027                        nsp.put( s.substring( 6, s.length() ), nnm.item( i ).getNodeValue() );
1028                    }
1029                }
1030            }
1031            NodeList nl = node.getChildNodes();
1032            if ( nl != null ) {
1033                for ( int i = 0; i < nl.getLength(); i++ ) {
1034                    collect( nsp, nl.item( i ) );
1035                }
1036            }
1037            return nsp;
1038        }
1039    
1040    }