001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/ogcwebservices/wfs/GetFeatureHandler.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2008 by:
006     EXSE, Department of Geography, University of Bonn
007     http://www.giub.uni-bonn.de/deegree/
008     lat/lon GmbH
009     http://www.lat-lon.de
010    
011     This library is free software; you can redistribute it and/or
012     modify it under the terms of the GNU Lesser General Public
013     License as published by the Free Software Foundation; either
014     version 2.1 of the License, or (at your option) any later version.
015    
016     This library is distributed in the hope that it will be useful,
017     but WITHOUT ANY WARRANTY; without even the implied warranty of
018     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
019     Lesser General Public License for more details.
020    
021     You should have received a copy of the GNU Lesser General Public
022     License along with this library; if not, write to the Free Software
023     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
024    
025     Contact:
026    
027     Andreas Poth
028     lat/lon GmbH
029     Aennchenstr. 19
030     53177 Bonn
031     Germany
032     E-Mail: poth@lat-lon.de
033    
034     Prof. Dr. Klaus Greve
035     Department of Geography
036     University of Bonn
037     Meckenheimer Allee 166
038     53115 Bonn
039     Germany
040     E-Mail: greve@giub.uni-bonn.de
041    
042     ---------------------------------------------------------------------------*/
043    package org.deegree.ogcwebservices.wfs;
044    
045    import java.util.ArrayList;
046    import java.util.List;
047    import java.util.concurrent.Callable;
048    import java.util.concurrent.CancellationException;
049    
050    import org.deegree.datatypes.QualifiedName;
051    import org.deegree.framework.concurrent.ExecutionFinishedEvent;
052    import org.deegree.framework.concurrent.Executor;
053    import org.deegree.framework.log.ILogger;
054    import org.deegree.framework.log.LoggerFactory;
055    import org.deegree.i18n.Messages;
056    import org.deegree.io.datastore.Datastore;
057    import org.deegree.io.datastore.schema.MappedFeatureType;
058    import org.deegree.model.feature.FeatureCollection;
059    import org.deegree.model.feature.GMLFeatureAdapter;
060    import org.deegree.model.feature.GMLFeatureCollectionDocument;
061    import org.deegree.ogcwebservices.OGCWebServiceException;
062    import org.deegree.ogcwebservices.wfs.capabilities.WFSCapabilities;
063    import org.deegree.ogcwebservices.wfs.capabilities.WFSFeatureType;
064    import org.deegree.ogcwebservices.wfs.capabilities.WFSOperationsMetadata;
065    import org.deegree.ogcwebservices.wfs.configuration.WFSConfiguration;
066    import org.deegree.ogcwebservices.wfs.operation.FeatureResult;
067    import org.deegree.ogcwebservices.wfs.operation.GetFeature;
068    import org.deegree.ogcwebservices.wfs.operation.Query;
069    import org.deegree.ogcwebservices.wfs.operation.GetFeature.RESULT_TYPE;
070    import org.deegree.owscommon.OWSDomainType;
071    
072    /**
073     * Handles {@link GetFeature} requests to the {@link WFService}. Since a {@link GetFeature} request
074     * may contain more than one {@link Query}, each {@link Query} is delegated to an own thread.
075     * <p>
076     * The results of all threads are collected and merged before they are returned to the calling
077     * {@link WFService} as a single {@link FeatureCollection}.
078     * 
079     * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
080     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
081     * @author last edited by: $Author: apoth $
082     * 
083     * @version $Revision: 9345 $, $Date: 2007-12-27 17:22:25 +0100 (Do, 27 Dez 2007) $
084     */
085    class GetFeatureHandler {
086    
087        private static final ILogger LOG = LoggerFactory.getLogger( GetFeatureHandler.class );
088    
089        private static final String EPSG_URL = "http://www.opengis.net/gml/srs/epsg.xml#";
090    
091        // upper limit for timeout (overrides WFS configuration)
092        private static long MAX_TIMEOUT_MILLIS = 60 * 60 * 1000;
093    
094        private WFService wfs;
095    
096        private int maxFeatures = -1;
097    
098        /**
099         * Creates a new instance of <code>GetFeatureHandler</code>. Only called by the associated
100         * {@link WFService} (once).
101         * 
102         * @param wfs
103         *            associated WFService
104         */
105        GetFeatureHandler( WFService wfs ) {
106            this.wfs = wfs;
107            WFSCapabilities capa = wfs.getCapabilities();
108            WFSOperationsMetadata md = (WFSOperationsMetadata) capa.getOperationsMetadata();
109            OWSDomainType[] dt = md.getConstraints();
110            for ( int i = 0; i < dt.length; i++ ) {
111                if ( dt[i].getName().equals( "DefaultMaxFeatures" ) ) {
112                    try {
113                        String tmp = dt[i].getValues()[0];
114                        this.maxFeatures = Integer.parseInt( tmp );
115                    } catch ( Exception e ) {
116                        // e.printStackTrace();
117                    }
118                    break;
119                }
120            }
121            LOG.logDebug( "default maxFeatures " + this.maxFeatures );
122        }
123    
124        /**
125         * Handles a {@link GetFeature} request by delegating the contained {@link Query} objects to
126         * different threads.
127         * <p>
128         * If at least one query fails an exception will be thrown and all running threads will be
129         * stopped.
130         * 
131         * @param getFeature
132         * @return result of the request
133         * @throws OGCWebServiceException
134         */
135        FeatureResult handleRequest( GetFeature getFeature )
136                                throws OGCWebServiceException {
137    
138            if ( getFeature.getMaxFeatures() > this.maxFeatures || getFeature.getMaxFeatures() <= 0 ) {
139                getFeature.setMaxFeatures( this.maxFeatures );
140            }
141    
142            LOG.logDebug( "maxFeatures " + getFeature.getMaxFeatures() );
143    
144            Query[] queries = getFeature.getQuery();
145            List<Callable<FeatureCollection>> queryTasks = new ArrayList<Callable<FeatureCollection>>( queries.length );
146    
147            for ( Query query : queries ) {
148    
149                QualifiedName[] ftNames = query.getTypeNames();
150                MappedFeatureType[] requestedFts = new MappedFeatureType[ftNames.length];
151                Datastore ds = null;
152    
153                for ( int i = 0; i < ftNames.length; i++ ) {
154                    QualifiedName ftName = ftNames[i];
155                    MappedFeatureType ft = this.wfs.getMappedFeatureType( ftName );
156    
157                    if ( ft == null ) {
158                        String msg = Messages.getMessage( "WFS_FEATURE_TYPE_UNKNOWN", ftName );
159                        throw new OGCWebServiceException( this.getClass().getName(), msg );
160                    }
161                    if ( ft.isAbstract() ) {
162                        String msg = Messages.getMessage( "WFS_FEATURE_TYPE_ABSTRACT", ftName );
163                        throw new OGCWebServiceException( this.getClass().getName(), msg );
164                    }
165                    if ( !ft.isVisible() ) {
166                        String msg = Messages.getMessage( "WFS_FEATURE_TYPE_INVISIBLE", ftName );
167                        throw new OGCWebServiceException( this.getClass().getName(), msg );
168                    }
169                    Datastore dsForFt = ft.getGMLSchema().getDatastore();
170                    if ( ds != null ) {
171                        if ( ds != dsForFt ) {
172                            String msg = Messages.getMessage( "WFS_QUERY_JOIN_OVER_DIFFERENT_DS" );
173                            throw new OGCWebServiceException( this.getClass().getName(), msg );
174                        }
175                    } else {
176                        ds = dsForFt;
177                    }
178                    requestedFts[i] = ft;
179                }
180    
181                // check and normalize requested SRS
182                // TODO what about joins here?
183                String srsName = query.getSrsName();
184                if ( srsName != null ) {
185                    WFSFeatureType wfsFT = this.wfs.getCapabilities().getFeatureTypeList().getFeatureType( ftNames[0] );
186                    String normalizedSrsName = normalizeSrsName( srsName );
187                    query.setSrsName( normalizedSrsName );
188    
189                    if ( !( wfsFT.supportsSrs( normalizedSrsName ) ) ) {
190                        String msg = Messages.getMessage( "WFS_FEATURE_TYPE_SRS_UNSUPPORTED", ftNames[0], srsName );
191                        throw new OGCWebServiceException( this.getClass().getName(), msg );
192                    }
193                }
194    
195                QueryTask task = new QueryTask( ds, query, requestedFts );
196                queryTasks.add( task );
197            }
198    
199            WFSConfiguration conf = (WFSConfiguration) wfs.getCapabilities();
200            long timeout = conf.getDeegreeParams().getRequestTimeLimit() * 1000;
201            if ( timeout > MAX_TIMEOUT_MILLIS ) {
202                // limit max timeout
203                timeout = MAX_TIMEOUT_MILLIS;
204            }
205    
206            List<ExecutionFinishedEvent<FeatureCollection>> finishedEvents = null;
207            try {
208                finishedEvents = Executor.getInstance().performSynchronously( queryTasks, timeout );
209            } catch ( InterruptedException e ) {
210                String msg = "Exception occured while waiting for the GetFeature results: " + e.getMessage();
211                throw new OGCWebServiceException( this.getClass().getName(), msg );
212            }
213    
214            // use id of the request as id of the result feature collection
215            // to allow identification of the original request that produced
216            // the feature collection
217            FeatureCollection fc = null;
218            if ( getFeature.getResultType() == RESULT_TYPE.RESULTS ) {
219                fc = mergeResults( getFeature.getId(), finishedEvents );
220            } else {
221                fc = mergeHits( getFeature.getId(), finishedEvents );
222            }
223    
224            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
225                try {
226                    GMLFeatureAdapter ada = new GMLFeatureAdapter( false );
227                    GMLFeatureCollectionDocument doc = ada.export( fc );
228                    LOG.logDebugXMLFile( "GetFeatureHandler_result", doc );
229                } catch ( Exception e ) {
230                    LOG.logError( e.getMessage(), e );
231                }
232            }
233    
234            FeatureResult fr = new FeatureResult( getFeature, fc );
235            return fr;
236        }
237    
238        /**
239         * Merges the results of the request subparts into one feature collection.
240         * 
241         * @param fcid
242         *            id of the new (result) feature collection
243         * @param finishedEvents
244         * @return feature collection containing all features from all responses
245         * @throws OGCWebServiceException
246         */
247        private FeatureCollection mergeResults( String fcid, List<ExecutionFinishedEvent<FeatureCollection>> finishedEvents )
248                                throws OGCWebServiceException {
249    
250            FeatureCollection result = null;
251    
252            try {
253                for ( ExecutionFinishedEvent<FeatureCollection> event : finishedEvents ) {
254                    if ( result == null ) {
255                        result = event.getResult();
256                    } else {
257                        result.addAll( event.getResult() );
258                    }
259                }
260            } catch ( CancellationException e ) {
261                LOG.logError( e.getMessage(), e );
262                String msg = Messages.getMessage( "WFS_GET_FEATURE_TIMEOUT", e.getMessage() );
263                throw new OGCWebServiceException( this.getClass().getName(), msg );
264            } catch ( Throwable t ) {
265                LOG.logError( t.getMessage(), t );
266                String msg = Messages.getMessage( "WFS_GET_FEATURE_BACKEND", t.getMessage() );
267                throw new OGCWebServiceException( this.getClass().getName(), msg );
268            }
269    
270            result.setId( fcid );
271            result.setAttribute( "numberOfFeatures", "" + result.size() );
272            return result;
273        }
274    
275        /**
276         * Merges the results of the request subparts into one feature collection.
277         * <p>
278         * This method is used if only the HITS have been requested, i.e. the number of features.
279         * 
280         * TODO: Do this a better way (maybe change feature model).
281         * 
282         * @param fcid
283         *            id of the new (result) feature collection
284         * @param finishedEvents
285         * @return empty feature collection with "numberOfFeatures" attribute
286         * @throws OGCWebServiceException
287         */
288        private FeatureCollection mergeHits( String fcid, List<ExecutionFinishedEvent<FeatureCollection>> finishedEvents )
289                                throws OGCWebServiceException {
290    
291            FeatureCollection result = null;
292            int numberOfFeatures = 0;
293    
294            try {
295                for ( ExecutionFinishedEvent<FeatureCollection> event : finishedEvents ) {
296                    FeatureCollection fc = event.getResult();
297                    try {
298                        numberOfFeatures += Integer.parseInt( ( fc.getAttribute( "numberOfFeatures" ) ) );
299                    } catch ( NumberFormatException e ) {
300                        String msg = "Internal error. Could not parse 'numberOfFeatures' attribute "
301                                     + "of sub-result as an integer value.";
302                        throw new OGCWebServiceException( this.getClass().getName(), msg );
303                    }
304                    if ( result == null ) {
305                        result = fc;
306                    } else {
307                        result.addAll( fc );
308                    }
309                }
310            } catch ( CancellationException e ) {
311                String msg = Messages.getMessage( "WFS_GET_FEATURE_TIMEOUT" );
312                LOG.logError( msg, e );
313                throw new OGCWebServiceException( this.getClass().getName(), msg );
314            } catch ( Throwable t ) {
315                String msg = Messages.getMessage( "WFS_GET_FEATURE_BACKEND", t.getMessage() );
316                LOG.logError( msg, t );
317                throw new OGCWebServiceException( this.getClass().getName(), msg );
318            }
319    
320            result.setId( fcid );
321            result.setAttribute( "numberOfFeatures", "" + numberOfFeatures );
322            return result;
323        }
324    
325        /**
326         * Returns a normalized version of the given srs identifier.
327         * <p>
328         * Names in the format: <code>http://www.opengis.net/gml/srs/epsg.xml#XYZ</code> are returned
329         * as <code>EPSG:XYZ</code>.
330         * 
331         * @param srsName
332         *            name of the srs, <code>EPSG:xyz</code>
333         * @return a normalized version of <code>srsName</code>
334         */
335        private String normalizeSrsName( String srsName ) {
336            String normalizedName = srsName;
337            if ( srsName.startsWith( EPSG_URL ) ) {
338                String epsgCode = srsName.substring( EPSG_URL.length() );
339                normalizedName = "EPSG:" + epsgCode;
340            }
341            return normalizedName;
342        }
343    
344        // ///////////////////////////////////////////////////////////////////////////
345        // inner classes
346        // ///////////////////////////////////////////////////////////////////////////
347    
348        /**
349         * Inner class for performing queries on a datastore.
350         */
351        private class QueryTask implements Callable<FeatureCollection> {
352    
353            private Datastore ds;
354    
355            private Query query;
356    
357            private MappedFeatureType[] fts;
358    
359            QueryTask( Datastore ds, Query query, MappedFeatureType[] fts ) {
360                this.ds = ds;
361                this.query = query;
362                this.fts = fts;
363            }
364    
365            /**
366             * Performs the associated {@link Query} and returns the result.
367             * 
368             * @return resulting feature collection
369             * @throws Exception
370             */
371            public FeatureCollection call()
372                                    throws Exception {
373                FeatureCollection result = this.ds.performQuery( query, fts );
374                return result;
375            }
376        }
377    }