    001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.4_testing/src/org/deegree/portal/standard/context/control/DownloadListener.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.portal.standard.context.control;
    038
    039    import java.io.File;
    040    import java.io.FileWriter;
    041    import java.io.IOException;
    042    import java.io.InputStreamReader;
    043    import java.net.MalformedURLException;
    044    import java.net.URI;
    045    import java.net.URL;
    046    import java.util.ArrayList;
    047    import java.util.Iterator;
    048    import java.util.List;
    049    import java.util.UUID;
    050
    051    import javax.servlet.ServletContext;
    052    import javax.servlet.http.HttpServletRequest;
    053    import javax.servlet.http.HttpSession;
    054
    055    import org.deegree.datatypes.QualifiedName;
    056    import org.deegree.enterprise.control.AbstractListener;
    057    import org.deegree.enterprise.control.FormEvent;
    058    import org.deegree.enterprise.control.RPCMember;
    059    import org.deegree.enterprise.control.RPCMethodCall;
    060    import org.deegree.enterprise.control.RPCParameter;
    061    import org.deegree.enterprise.control.RPCStruct;
    062    import org.deegree.enterprise.control.RPCWebEvent;
    063    import org.deegree.framework.log.ILogger;
    064    import org.deegree.framework.log.LoggerFactory;
    065    import org.deegree.framework.mail.EMailMessage;
    066    import org.deegree.framework.mail.MailHelper;
    067    import org.deegree.framework.mail.MailMessage;
    068    import org.deegree.framework.mail.SendMailException;
    069    import org.deegree.framework.util.FeatureUtils;
    070    import org.deegree.framework.util.IDGenerator;
    071    import org.deegree.framework.util.NetWorker;
    072    import org.deegree.framework.util.StringTools;
    073    import org.deegree.framework.util.ZipUtils;
    074    import org.deegree.framework.xml.Marshallable;
    075    import org.deegree.framework.xml.NamespaceContext;
    076    import org.deegree.framework.xml.XMLFragment;
    077    import org.deegree.framework.xml.XMLParsingException;
    078    import org.deegree.framework.xml.XMLTools;
    079    import org.deegree.i18n.Messages;
    080    import org.deegree.io.shpapi.ShapeFile;
    081    import org.deegree.model.feature.Feature;
    082    import org.deegree.model.feature.FeatureCollection;
    083    import org.deegree.model.feature.FeatureProperty;
    084    import org.deegree.model.feature.GMLFeatureCollectionDocument;
    085    import org.deegree.model.filterencoding.ComplexFilter;
    086    import org.deegree.model.filterencoding.Filter;
    087    import org.deegree.model.filterencoding.Operation;
    088    import org.deegree.model.filterencoding.OperationDefines;
    089    import org.deegree.model.filterencoding.PropertyName;
    090    import org.deegree.model.filterencoding.SpatialOperation;
    091    import org.deegree.model.spatialschema.Envelope;
    092    import org.deegree.model.spatialschema.Geometry;
    093    import org.deegree.model.spatialschema.GeometryFactory;
    094    import org.deegree.ogcbase.BaseURL;
    095    import org.deegree.ogcbase.CommonNamespaces;
    096    import org.deegree.ogcwebservices.OWSUtils;
    097    import org.deegree.ogcwebservices.wfs.capabilities.WFSCapabilities;
    098    import org.deegree.ogcwebservices.wfs.capabilities.WFSOperationsMetadata;
    099    import org.deegree.ogcwebservices.wfs.operation.GetFeature;
    100    import org.deegree.ogcwebservices.wfs.operation.GetFeatureDocument;
    101    import org.deegree.ogcwebservices.wfs.operation.Query;
    102    import org.deegree.ogcwebservices.wfs.operation.GetFeature.RESULT_TYPE;
    103    import org.deegree.owscommon.OWSDomainType;
    104    import org.deegree.portal.Constants;
    105    import org.deegree.portal.PortalException;
    106    import org.deegree.portal.context.GeneralExtension;
    107    import org.deegree.portal.context.Layer;
    108    import org.deegree.portal.context.LayerExtension;
    109    import org.deegree.portal.context.ViewContext;
    110    import org.xml.sax.SAXException;
    111
    112    /**
    113     * This Listener is used when a user likes to download the WFS data behind a WMS layer.
    114     *
    115     * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
    116     * @author last edited by: $Author: jmays $
    117     *
    118     * @version $Revision: 22739 $ $Date: 2010-02-24 16:17:39 +0100 (Mi, 24. Feb 2010) $
    119     */
    120    public class DownloadListener extends AbstractListener {
    121
    122        private static final ILogger LOG = LoggerFactory.getLogger( DownloadListener.class );
    123
    124        private static final NamespaceContext nsContext = CommonNamespaces.getNamespaceContext();
    125
    126        private String mailAddr = "";
    127
    128        private String downloadDir = "";
    129
    130        private URL downloadURL = null;
    131
    132        /**
    133         * default is false. A value may be provided as TEST_MAX_HITS in initParameters of controller.xml
    134         */
    135        private boolean isMaxFeaturesTestEnabled = false;
    136
    137        /**
    138         * default value for maximum number of features that shall be retreived from a WFS, as provided in initParameters of
    139         * controller.xml
    140         */
    141        private int defaultMaxFeatures = 0;
    142
    143        /*
    144         * (non-Javadoc)
    145         *
    146         * @see org.deegree.enterprise.control.AbstractListener#actionPerformed(org.deegree.enterprise.control.FormEvent)
    147         */
    148        @Override
    149        public void actionPerformed( FormEvent event ) {
    150
    151            // set init parameter as provided in controller.xml
    152            if ( Integer.getInteger( getInitParameter( "DEFAULT_MAX_FEATURES" ) ) != null ) {
    153                defaultMaxFeatures = Integer.getInteger( getInitParameter( "DEFAULT_MAX_FEATURES" ) ).intValue();
    154            }
    155            isMaxFeaturesTestEnabled = "true".equalsIgnoreCase( getInitParameter( "TEST_MAX_HITS" ) );
    156
    157            RPCWebEvent rpc = (RPCWebEvent) event;
    158            try {
    159                validate( rpc );
    160            } catch ( PortalException e ) {
    161                gotoErrorPage( Messages.get( "IGEO_STD_CNTXT_INVALID_RPC", "download", e.getMessage() ) );
    162                return;
    163            }
    164
    165            try {
    166                // get users email address
    167                mailAddr = getUserEmail( rpc );
    168            } catch ( Exception e ) {
    169                LOG.logError( e.getMessage(), e );
    170                gotoErrorPage( e.getMessage() );
    171                return;
    172            }
    173            if ( mailAddr == null ) {
    174                gotoErrorPage( "Unknown user and/or email address. Download not possible." );
    175                return;
    176            }
    177
    178            // read base context
    179            HttpSession session = ( (HttpServletRequest) this.getRequest() ).getSession( true );
    180            ViewContext vc = (ViewContext) session.getAttribute( Constants.CURRENTMAPCONTEXT );
    181
    182            downloadDir = ( (GeneralExtension) vc.getGeneral().getExtension() ).getIOSettings().getDownloadDirectory().getDirectoryName();
    183            downloadURL = ( (GeneralExtension) vc.getGeneral().getExtension() ).getIOSettings().getDownloadDirectory().getOnlineResource();
    184
    185            ServletContext sc = session.getServletContext();
    186            sc.setAttribute( Constants.DOWNLOADDIR, downloadDir );
    187
    188            String msg = "";
    189            if ( !msg.equals( "" ) ) {
    190                // user doesn't have the authorization to download the ordered datasets
    191                this.setNextPage( this.getAlternativeNextPage() );
    192                this.getRequest().setAttribute( Constants.MESSAGE, msg );
    193            } else {
    194                try {
    195                    ArrayList<RequestBean> gfrl = new ArrayList<RequestBean>();
    196                    ArrayList<FeatureTemplate> fids = createFeatureTemplates( rpc );
    197                    Iterator<FeatureTemplate> iterator = fids.iterator();
    198
    199                    // create a GetFeature request for each ordered dataset
    200                    while ( iterator.hasNext() ) {
    201                        FeatureTemplate ft = iterator.next();
    202                        RequestBean gfRequest = getWFSGetFeatureCalls( ft, vc );
    203
    204                        if ( gfRequest == null ) {
    205                            StringBuffer s = new StringBuffer( 500 );
    206                            try {
    207                                s.append( Messages.get( "IGEO_STD_CNTXT_MISSING_DATA" ) );
    208                                s.append( "<BR><BR>" );
    209                            } catch ( Exception e ) {
    210                                LOG.logError( e.getMessage(), e );
    211                            }
    212                            s.append( "<b>" ).append( ft.getTitle() ).append( "</b>" );
    213                            throw new Exception( s.toString() );
    214                        } else {
    215                            gfrl.add( gfRequest );
    216                        }
    217                    }
    218
    219                    // perform dataloading in another thread
    220                    RPCStruct struct = (RPCStruct) rpc.getRPCMethodCall().getParameters()[0].getValue();
    221                    String format = (String) struct.getMember( "format" ).getValue();
    222                    LoadController cntr = new LoadController( gfrl, format );
    223                    cntr.start();
    224                    String s = Messages.get( "IGEO_STD_CNTXT_INFO_EMAIL_CONFIRM", mailAddr );
    225                    this.setNextPage( "message.jsp" );
    226                    getRequest().setAttribute( Constants.MESSAGE, s );
    227                } catch ( Exception e ) {
    228                    LOG.logError( e.getMessage(), e );
    229                    this.getRequest().setAttribute( Constants.MESSAGE, e.toString() );
    230                    gotoErrorPage( e.toString() );
    231                }
    232            }
    233        }
    234
    235        private String getUserEmail( RPCWebEvent rpc )
    236                                throws Exception {
    237            String email = null;
    238
    239            RPCStruct struct = (RPCStruct) rpc.getRPCMethodCall().getParameters()[0].getValue();
    240            String sessionId = (String) struct.getMember( "sessionID" ).getValue(); // this is the userName
    241            String rpcMail = (String) struct.getMember( "email" ).getValue();
    242
    243            if ( ( sessionId == null || "null".equals( sessionId ) ) && ( rpcMail == null || "".equals( rpcMail ) ) ) {
    244                // unknown session AND unknown email address.
    245                // -> no way to find out where to send the download-info.
    246                LOG.logDebug( "Neither sessionID nor email was set in RPC params" );
    247                return null;
    248            }
    249
    250            HttpSession session = ( (HttpServletRequest) getRequest() ).getSession( true );
    251            ViewContext vc = (ViewContext) session.getAttribute( Constants.CURRENTMAPCONTEXT );
    252            if ( vc == null ) {
    253                return null;
    254            }
    255            GeneralExtension ge = vc.getGeneral().getExtension();
    256
    257            if ( sessionId != null && !"null".equals( sessionId ) && ge.getAuthentificationSettings() != null ) {
    258                LOG.logDebug( "try getting user information (email address) from WAS/sessionID" );
    259                BaseURL baseUrl = ge.getAuthentificationSettings().getAuthentificationURL();
    260                String url = OWSUtils.validateHTTPGetBaseURL( baseUrl.getOnlineResource().toExternalForm() );
    261                StringBuffer sb = new StringBuffer( url );
    262                sb.append( "request=DescribeUser&SESSIONID=" ).append( sessionId );
    263
    264                XMLFragment xml = new XMLFragment();
    265                xml.load( new URL( sb.toString() ) );
    266
    267                email = XMLTools.getRequiredNodeAsString( xml.getRootElement(), "/User/EMailAddress", nsContext );
    268            }
    269            if ( email == null || ( "" ).equals( email ) ) {
    270                LOG.logDebug( "Taking email address from rpc request!" );
    271                email = rpcMail;
    272            }
    273
    274            LOG.logDebug( "Email: " + email );
    275            return email;
    276        }
    277
    278        /**
    279         * gets the user name assigned to the passed session ID from a authentification service. If no user is assigned to
    280         * the session ID <tt>null</tt> will be returned. If the session is closed or expired an exception will be thrown
    281         *
    282         * @param sessionId
    283         * @return name of the user assigned to the passed session ID
    284         */
    285        protected String getUserName( String sessionId )
    286                                throws XMLParsingException, IOException, SAXException {
    287
    288            HttpSession session = ( (HttpServletRequest) getRequest() ).getSession( true );
    289            ViewContext vc = (ViewContext) session.getAttribute( Constants.CURRENTMAPCONTEXT );
    290            if ( vc == null ) {
    291                return null;
    292            }
    293            GeneralExtension ge = vc.getGeneral().getExtension();
    294            String userName = null;
    295            if ( sessionId != null && ge.getAuthentificationSettings() != null ) {
    296                LOG.logDebug( "try getting user from WAS/sessionID" );
    297                BaseURL baseUrl = ge.getAuthentificationSettings().getAuthentificationURL();
    298                String url = OWSUtils.validateHTTPGetBaseURL( baseUrl.getOnlineResource().toExternalForm() );
    299                StringBuffer sb = new StringBuffer( url );
    300                sb.append( "request=DescribeUser&SESSIONID=" ).append( sessionId );
    301
    302                XMLFragment xml = new XMLFragment();
    303                xml.load( new URL( sb.toString() ) );
    304
    305                userName = XMLTools.getRequiredNodeAsString( xml.getRootElement(), "/User/UserName", nsContext );
    306            } else {
    307                LOG.logDebug( "try getting user from getUserPrincipal()" );
    308                if ( ( (HttpServletRequest) getRequest() ).getUserPrincipal() != null ) {
    309                    userName = ( (HttpServletRequest) getRequest() ).getUserPrincipal().getName();
    310                    if ( userName.indexOf( "\\" ) > 1 ) {
    311                        String[] us = StringTools.toArray( userName, "\\", false );
    312                        userName = us[us.length - 1];
    313                    }
    314                }
    315            }
    316            LOG.logDebug( "userName: " + userName );
    317            return userName;
    318        }
    319
    320        /**
    321         * Convenience method to extract the boundig box from an rpc fragment.
    322         *
    323         * @param bboxStruct
    324         *            the <code>RPCStruct</code> containing the bounding box. For example,
    325         *            <code>&lt;member&gt;&lt;name&gt;boundingBox&lt;/name&gt;etc...</code>.
    326         *
    327         * @return an envelope with the boundaries defined in the rpc structure
    328         */
    329        protected Envelope extractBBox( RPCStruct bboxStruct ) {
    330
    331            // read base context
    332            HttpSession session = ( (HttpServletRequest) this.getRequest() ).getSession( true );
    333            ViewContext vc = (ViewContext) session.getAttribute( Constants.CURRENTMAPCONTEXT );
    334            Double minx = (Double) bboxStruct.getMember( Constants.RPC_BBOXMINX ).getValue();
    335            Double miny = (Double) bboxStruct.getMember( Constants.RPC_BBOXMINY ).getValue();
    336            Double maxx = (Double) bboxStruct.getMember( Constants.RPC_BBOXMAXX ).getValue();
    337            Double maxy = (Double) bboxStruct.getMember( Constants.RPC_BBOXMAXY ).getValue();
    338
    339            Envelope bbox = GeometryFactory.createEnvelope( minx.doubleValue(), miny.doubleValue(), maxx.doubleValue(),
    340                                                            maxy.doubleValue(),
    341                                                            vc.getGeneral().getBoundingBox()[0].getCoordinateSystem() );
    342            return bbox;
    343        }
    344
    345        /**
    346         * validates the request to be performed
    347         */
    348        private void validate( RPCWebEvent rpc )
    349                                throws PortalException {
    350
    351            RPCMethodCall mc = rpc.getRPCMethodCall();
    352            RPCParameter param = mc.getParameters()[0];
    353            RPCStruct struct = (RPCStruct) param.getValue();
    354
    355            RPCMember layerList = struct.getMember( "layerList" );
    356            RPCMember sessionID = struct.getMember( "sessionID" );
    357            RPCMember email = struct.getMember( "email" );
    358            RPCMember format = struct.getMember( "format" );
    359            RPCMember boundingBox = struct.getMember( "boundingBox" );
    360
    361            if ( layerList == null ) {
    362                throw new PortalException( Messages.get( "IGEO_STD_CNTXT_MISSING_PARAM", "'layerList'", "Download" ) );
    363            }
    364            if ( sessionID == null ) {
    365                throw new PortalException( Messages.get( "IGEO_STD_CNTXT_MISSING_PARAM", "'sessionID'", "Download" ) );
    366            }
    367            if ( email == null ) {
    368                // has been added to RPC in deegree 2.4
    369                throw new PortalException( Messages.get( "IGEO_STD_CNTXT_MISSING_PARAM", "'email'", "Download" ) );
    370            }
    371            if ( format == null ) {
    372                // has been added to RPC in deegree 2.4
    373                throw new PortalException( Messages.get( "IGEO_STD_CNTXT_MISSING_PARAM", "'format'", "Download" ) );
    374            }
    375            if ( boundingBox == null ) {
    376                throw new PortalException( Messages.get( "IGEO_STD_CNTXT_MISSING_PARAM", "'boundingBox'", "Download" ) );
    377            }
    378
    379            // DEBUG STATEMENTS
    380            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
    381                RPCStruct layerStruct = (RPCStruct) layerList.getValue();
    382                RPCMember[] layers = layerStruct.getMembers();
    383                for ( int i = 0; i < layers.length; i++ ) {
    384                    // key = "layername,layertitle" , value = "WMS-Service-Url"
    385                    String tmp = layers[i].getName();
    386                    String layerName = tmp.substring( 0, tmp.indexOf( "," ) );
    387                    String layerTitle = tmp.substring( tmp.indexOf( "," ) + 1 );
    388                    String layerServiceURL = (String) layers[i].getValue();
    389                    LOG.logDebug( "layername: ", layerName, "  layertitle: ", layerTitle, "  url: ", layerServiceURL );
    390                }
    391                LOG.logDebug( "sessionID=" + (String) sessionID.getValue() );
    392                LOG.logDebug( "email=" + (String) email.getValue() );
    393                LOG.logDebug( "format=" + (String) format.getValue() );
    394                RPCStruct bboxStruct = (RPCStruct) boundingBox.getValue();
    395                LOG.logDebug( "bbox minx=" + (Double) bboxStruct.getMember( Constants.RPC_BBOXMINX ).getValue() );
    396                LOG.logDebug( "bbox miny=" + (Double) bboxStruct.getMember( Constants.RPC_BBOXMINY ).getValue() );
    397                LOG.logDebug( "bbox maxx=" + (Double) bboxStruct.getMember( Constants.RPC_BBOXMAXX ).getValue() );
    398                LOG.logDebug( "bbox maxy=" + (Double) bboxStruct.getMember( Constants.RPC_BBOXMAXY ).getValue() );
    399            }
    400        }
    401
    402        /**
    403         * performs the access to the data marked at the shopping card
    404         */
    405        protected ArrayList<FeatureTemplate> createFeatureTemplates( RPCWebEvent event )
    406                                throws PortalException, Exception {
    407
    408            RPCParameter[] params = event.getRPCMethodCall().getParameters();
    409            RPCStruct struct = (RPCStruct) params[0].getValue();
    410            RPCStruct layerStruct = (RPCStruct) struct.getMember( "layerList" ).getValue();
    411
    412            RPCStruct bboxStruct = (RPCStruct) struct.getMember( Constants.RPC_BBOX ).getValue();
    413            Envelope bbox = extractBBox( bboxStruct );
    414
    415            RPCMember[] layers = layerStruct.getMembers();
    416
    417            ArrayList<FeatureTemplate> fids = new ArrayList<FeatureTemplate>( layers.length );
    418            for ( int i = 0; i < layers.length; i++ ) {
    419                // key = "layername,layertitle" , value = "WMS-Service-Url"
    420                String tmp = layers[i].getName();
    421                String layerName = tmp.substring( 0, tmp.indexOf( "," ) ); // assuming there is no "," within the layername
    422                String layerTitle = tmp.substring( tmp.indexOf( "," ) + 1 );
    423                String layerServiceURL = (String) layers[i].getValue();
    424                FeatureTemplate ft = new FeatureTemplate( layerName, layerTitle, bbox, layerServiceURL );
    425                fids.add( i, ft );
    426            }
    427            LOG.logDebug( "created FeatureTemplate.length=", fids.size() );
    428            return fids;
    429        }
    430
    431        /**
    432         * returns the call to be used to perform a GetFeature request for the passed feature template.
    433         *
    434         * @param layer
    435         *            used
    436         * @param vc
    437         * @return the call to be used to perform a GetFeature request for the passed feature template
    438         * @throws PortalException
    439         */
    440        protected RequestBean getWFSGetFeatureCalls( FeatureTemplate layer, ViewContext vc )
    441                                throws PortalException {
    442
    443            String href = null;
    444            String version = null;
    445            RequestBean result = null;
    446
    447            try {
    448                Layer contxtLayer = vc.getLayerList().getLayer( layer.getId(), layer.getServerURL() );
    449                LayerExtension lExt = null;
    450                try {
    451                    lExt = (LayerExtension) contxtLayer.getExtension();
    452                } catch ( ClassCastException e ) {
    453                    throw new PortalException( e.getMessage() );
    454                }
    455
    456                href = lExt.getDataService().getServer().getOnlineResource().toString();
    457                version = lExt.getDataService().getServer().getVersion();
    458
    459                // feature from layer extension
    460                String featureType = lExt.getDataService().getFeatureType();
    461                String nsFeature = featureType.substring( featureType.indexOf( '{' ) + 1, featureType.indexOf( '}' ) );
    462                String featureLocalName = featureType.substring( featureType.lastIndexOf( ':' ) + 1 );
    463
    464                // geometry from layer extension
    465                String geometryType = lExt.getDataService().getGeometryType();
    466                String nsGeometry = geometryType.substring( geometryType.indexOf( '{' ) + 1, geometryType.indexOf( '}' ) );
    467                String geometryLocalName = geometryType.substring( geometryType.lastIndexOf( ':' ) + 1 );
    468
    469                if ( href != null ) {
    470                    // init RequestBean values
    471                    GetFeature gfrHits = null;
    472                    GetFeature gfr = null;
    473                    int maxRequest = defaultMaxFeatures;
    474
    475                    if ( isMaxFeaturesTestEnabled ) {
    476                        // get the DefaultMaxFeatures from the capabilities of the wfs.
    477                        if ( !( lExt.getDataService().getServer().getCapabilities() instanceof WFSCapabilities ) ) {
    478                            LOG.logDebug( "very strange, should be a wfs capa." );
    479                            // FIXME do something intelligent
    480                        } else {
    481                            WFSCapabilities cap = ( (WFSCapabilities) lExt.getDataService().getServer().getCapabilities() );
    482                            WFSOperationsMetadata opm = ( (WFSOperationsMetadata) cap.getOperationsMetadata() );
    483                            OWSDomainType[] constraints = opm.getConstraints();
    484                            for ( OWSDomainType cons : constraints ) {
    485                                if ( "DefaultMaxFeatures".equals( cons.getName() ) ) {
    486                                    String[] values = cons.getValues();
    487                                    if ( values != null ) {
    488                                        // take the freaking first.
    489                                        try {
    490                                            maxRequest = Integer.parseInt( values[0] );
    491                                        } catch ( NumberFormatException e ) {
    492                                            // do nothing
    493                                        }
    494                                    }
    495                                }
    496                            }
    497                        }
    498                        // RESULT_TYPE = HITS
    499                        gfrHits = createGetFeatureRequest( layer, version, new QualifiedName( "app", featureLocalName,
    500                                                                                              new URI( nsFeature ) ),
    501                                                           new QualifiedName( "app", geometryLocalName,
    502                                                                              new URI( nsGeometry ) ), RESULT_TYPE.HITS );
    503                    }
    504                    // RESULT_TYPE = RESULTS
    505                    gfr = createGetFeatureRequest( layer, version, new QualifiedName( "app", featureLocalName,
    506                                                                                      new URI( nsFeature ) ),
    507                                                   new QualifiedName( "app", geometryLocalName, new URI( nsGeometry ) ),
    508                                                   RESULT_TYPE.RESULTS );
    509
    510                    result = new RequestBean( href, gfrHits, gfr, maxRequest, layer.getId(), layer.getTitle() );
    511                }
    512            } catch ( Exception e ) {
    513                LOG.logError( e.getMessage(), e );
    514                throw new PortalException( e.getMessage(), e );
    515            }
    516            return result;
    517        }
    518
    519        /**
    520         * creates a GetFeature request considering the feature type (ID) and the bounding box encapsulated in the passed
    521         * <tt>FeatureTemplate</tt>
    522         *
    523         * @param ft
    524         *            FeatureTemplate
    525         * @param ftName
    526         *            a Qualified Name representing the feature type name of the requested feature, ex: app:ZipCodes
    527         * @param gtName
    528         *            a Qualified Name representing the geometry type name of the requested feature, ex: app:geometry
    529         * @param resultType
    530         *            the type of result (GetFeature.RESULT_TYPE.HITS or GetFeature.RESULT_TYPE.RESULTS)
    531         * @return {@link GetFeature} request to access data for download
    532         * @throws PortalException
    533         */
    534        protected GetFeature createGetFeatureRequest( FeatureTemplate ft, String version, QualifiedName ftName,
    535                                                      QualifiedName gtName, GetFeature.RESULT_TYPE resultType )
    536                                throws PortalException {
    537
    538            // read base context
    539            HttpSession session = ( (HttpServletRequest) this.getRequest() ).getSession( true );
    540            ViewContext vc = (ViewContext) session.getAttribute( Constants.CURRENTMAPCONTEXT );
    541            String srcName = vc.getGeneral().getBoundingBox()[0].getCoordinateSystem().getIdentifier();
    542
    543            GetFeature gfr = null;
    544
    545            try {
    546                Geometry geom = GeometryFactory.createSurface( ft.getEnvelope(), ft.getEnvelope().getCoordinateSystem() );
    547                Operation op = new SpatialOperation( OperationDefines.BBOX, new PropertyName( gtName ), geom );
    548                Filter filter = new ComplexFilter( op );
    549                IDGenerator idg = IDGenerator.getInstance();
    550
    551                // create WFS GetFeature request for either HITS or RESULTS
    552                Query query = Query.create( null, null, null, null, version, new QualifiedName[] { ftName }, null, srcName,
    553                                            filter, -1, 0, resultType );
    554                gfr = GetFeature.create( "1.1.0", "" + idg.generateUniqueID(), resultType, "text/xml; subtype=gml/3.1.1",
    555                                         null, -1, 0, -1, 0, new Query[] { query } );
    556            } catch ( Exception e ) {
    557                LOG.logError( e.getMessage(), e );
    558                throw new PortalException( Messages.get( "IGEO_STD_CNTXT_ERROR_CREATE_GETFEATURE", ft.getTitle(),
    559                                                         ft.getEnvelope() ) );
    560            }
    561            return gfr;
    562        }
    563
    564        // /////////////////////////////////////////////////////////////////////////
    565        // inner class //
    566        // /////////////////////////////////////////////////////////////////////////
    567
    568        /**
    569         * class that handles the loading of the requested data from the responsible WFS. The class also informs the user
    570         * who had requested the data via email that the data are ready for download if everything had worked fine.
    571         * otherwise the the user and the administrator will be informed about the problem that had occured.
    572         */
    573        private class LoadController extends Thread {
    574
    575            private ArrayList<RequestBean> gfrbl = null;
    576
    577            private String errorFeatures = null;
    578
    579            private String errorHitsException = null;
    580
    581            /**
    582             * default format for data download is SHP
    583             */
    584            private String format = "SHP";
    585
    586            /**
    587             * initialize the LoadController with an array list of GetFeature requests beans and the download format
    588             */
    589            LoadController( ArrayList<RequestBean> gfrl, String format ) {
    590                this.gfrbl = gfrl;
    591                this.format = format;
    592            }
    593
    594            /*
    595             * (non-Javadoc)
    596             *
    597             * @see java.lang.Thread#run()
    598             */
    599            @Override
    600            public void run() {
    601                ArrayList<String> zipFiles = new ArrayList<String>();
    602                Iterator<RequestBean> iterator = gfrbl.iterator();
    603
    604                while ( iterator.hasNext() ) {
    605                    RequestBean request = iterator.next();
    606                    String wfsAddr = request.serverLocation;
    607                    GetFeature gfHits = request.hitsRequest; // is NULL if !isMaxFeaturesTestEnabled
    608                    GetFeature gfResults = request.request;
    609                    String layerName = request.layerName;
    610                    String layerTitle = request.layerTitle;
    611
    612                    FeatureCollection fcHits = null;
    613                    FeatureCollection fcResults = null;
    614                    GMLFeatureCollectionDocument gmlDoc = null;
    615                    boolean isHitsException = false;
    616
    617                    try {
    618                        URL url = new URL( wfsAddr );
    619
    620                        if ( isMaxFeaturesTestEnabled ) {
    621                            int numberOfHits = request.maxFeatures;
    622                            // get number of hits of features in a given BBOX from WFS
    623                            GetFeatureDocument doc = org.deegree.ogcwebservices.wfs.XMLFactory.export( gfHits );
    624                            LOG.logDebug( "GetFeature request:\n", doc.getAsPrettyString() );
    625                            NetWorker nw = new NetWorker( url, doc.getAsString() );
    626                            // extract numberOfFeatures
    627                            gmlDoc = new GMLFeatureCollectionDocument();
    628                            gmlDoc.load( new InputStreamReader( nw.getInputStream() ), XMLFragment.DEFAULT_URL );
    629
    630                            if ( "ServiceExceptionReport".equals( gmlDoc.getRootElement().getLocalName() )
    631                                 || "ExceptionReport".equals( gmlDoc.getRootElement().getLocalName() ) ) {
    632                                // if number of hits cannot be retreived from the WFS, then it is unsure whether the
    633                                // reseult set will contain all feature objects within the given BBOX, or merely a subset
    634                                // defined by the WFS settings.
    635                                LOG.logDebug( "Unknown number of Features in the selected area." );
    636                                isHitsException = true; // -> inform the user to contact the admin
    637                            } else {
    638                                fcHits = gmlDoc.parse();
    639                                numberOfHits = Integer.parseInt( fcHits.getAttribute( "numberOfFeatures" ) );
    640                            }
    641                            if ( isHitsException ) {
    642                                // inform the user to contact the admin
    643                                LOG.logDebug( "ServiceException for layer " + layerName );
    644                                if ( errorHitsException != null && errorHitsException.length() > 0 ) {
    645                                    errorHitsException += "\n" + layerName;
    646                                } else {
    647                                    errorHitsException = layerName;
    648                                }
    649                            } else if ( numberOfHits > request.maxFeatures ) {
    650                                // store info on featureTypes with to many features requested
    651                                LOG.logDebug( Messages.get( "IGEO_STD_CNTXT_TO_MANY_OBJECTS" ), " - ", layerTitle );
    652
    653                                if ( errorFeatures != null && errorFeatures.length() > 0 ) {
    654                                    errorFeatures += "\n" + layerTitle;
    655                                } else {
    656                                    errorFeatures = layerTitle;
    657                                }
    658                            } else {
    659                                // get feature data from WFS
    660                                doc = org.deegree.ogcwebservices.wfs.XMLFactory.export( gfResults );
    661                                LOG.logDebug( "GetFeature request:\n", doc.getAsPrettyString() );
    662                                nw = new NetWorker( url, doc.getAsString() );
    663                                // transform data and store as shape file
    664                                gmlDoc = new GMLFeatureCollectionDocument();
    665                                gmlDoc.load( new InputStreamReader( nw.getInputStream() ), XMLFragment.DEFAULT_URL );
    666                                fcResults = gmlDoc.parse();
    667                            }
    668
    669                        } else {
    670                            // gfHits = NULL && !isMaxFeaturesTestEnabled
    671                            // get feature data from WFS
    672                            GetFeatureDocument doc = org.deegree.ogcwebservices.wfs.XMLFactory.export( gfResults );
    673                            LOG.logDebug( "GetFeature request:\n", doc.getAsPrettyString() );
    674                            NetWorker nw = new NetWorker( url, doc.getAsString() );
    675                            // transform data and store as shape file
    676                            gmlDoc = new GMLFeatureCollectionDocument();
    677                            gmlDoc.load( new InputStreamReader( nw.getInputStream() ), XMLFragment.DEFAULT_URL );
    678                            fcResults = gmlDoc.parse();
    679                        }
    680                    } catch ( Exception e ) {
    681                        LOG.logError( e.getMessage(), e );
    682                        // remove all created zip files
    683                        // rollback( zipFiles, downloadDir );
    684                        // LOG.logError( "create shape file: " + gfResults.getQuery()[0].getTypeNames(), e );
    685                        try {
    686                            sendErrorMail( e, wfsAddr, ( (Marshallable) gfResults ).exportAsXML() );
    687                        } catch ( Exception e2 ) {
    688                            LOG.logError( e2.getMessage(), e2 );
    689                        }
    690                        return;
    691                    }
    692
    693                    File fileDir = null;
    694                    try {
    695                        fileDir = new File( new URL( downloadDir ).getFile() );
    696                    } catch ( MalformedURLException e ) {
    697                        LOG.logDebug( "Download dir did not start with 'file', trying just as file now.", e );
    698                        fileDir = new File( downloadDir );
    699                    }
    700                    LOG.logDebug( "download directory (fileDir): ", fileDir.toString() );
    701
    702                    try {
    703                        if ( fcResults != null && !isHitsException && !( fcResults.size() == 0 ) ) {
    704                            // add name of the created zipfile to an ArrayList to inform the user where to download the data
    705                            if ( "GML".equals( format ) ) {
    706                                String filename = storeGML( gmlDoc, layerName, fileDir );
    707                                zipFiles.add( filename );
    708                            } else if ( "SHP".equals( format ) ) {
    709                                List<String> filenames = storeFC( fcResults, layerName, fileDir );
    710                                zipFiles.addAll( filenames );
    711                            } else {
    712                                // fall back: default behaviour = SHAPE-Download
    713                                List<String> filenames = storeFC( fcResults, layerName, fileDir );
    714                                zipFiles.addAll( filenames );
    715                            }
    716                        }
    717                    } catch ( Exception e ) {
    718                        // remove all created zip files
    719                        rollback( zipFiles, downloadDir );
    720                        LOG.logError( "create zip file", e );
    721                        try {
    722                            String typeNames = arrayToString( gfResults.getQuery()[0].getTypeNames(), "," );
    723                            sendErrorMail( e, null, typeNames );
    724                        } catch ( Exception e2 ) {
    725                            LOG.logError( e2.getMessage(), e );
    726                        }
    727                        return;
    728                    }
    729                } // END WHILE
    730
    731                try {
    732                    sendSuccessMail( zipFiles );
    733                } catch ( Exception e ) {
    734                    LOG.logError( "sendSuccessMail", e );
    735                }
    736            }
    737
    738            private String arrayToString( QualifiedName[] strings, String delimiter ) {
    739                StringBuffer sb = new StringBuffer( 200 );
    740                for ( int i = 0; i < strings.length; i++ ) {
    741                    sb.append( strings[i] );
    742                    if ( i < strings.length ) {
    743                        sb.append( delimiter );
    744                    }
    745                }
    746                return sb.toString();
    747            }
    748
    749            /**
    750             * stores the passed feature collection as zipped shape and returns the file name(s)
    751             *
    752             * @param fc
    753             *            feature collection to be stored
    754             * @param layerName
    755             *            name of the WMS layer corresponding to the feature type requested from the WFS. It is used for
    756             *            file name.
    757             * @param fileDir
    758             *            the name of the download directory, as set in the WMC
    759             * @return a list of strings containing all the file names for the input feature collection. These may be more
    760             *         than on, as a feature collection might contain several geometry types which will be turned into
    761             *         different shape files: one for point, one for curves, ...
    762             * @throws PortalException
    763             */
    764            private List<String> storeFC( FeatureCollection fc, String layerName, File fileDir )
    765                                    throws PortalException {
    766
    767                String zipName = null;
    768                String[] shapeFileNames = null;
    769
    770                truncatePropertyValues( fc, 127, "..." ); // these values could be externalised to init params, if need be.
    771                // prevent creating shapes with multiple geometry types
    772                FeatureCollection[] fcs = FeatureUtils.separateFeaturesForShapes( fc );
    773
    774                List<String> files = new ArrayList<String>();
    775                for ( int i = 0; i < fcs.length - 1; i++ ) {
    776                    // only using the first four geometry types: point, multipoint, (multi-)curve, (multi-)surface.
    777                    if ( fcs[i].size() > 0 ) {
    778                        try {
    779                            // create and write shape
    780                            String shapeBaseName = layerName + System.currentTimeMillis() + fcs[i].getId();
    781                            File fileBaseName = new File( fileDir, shapeBaseName );
    782                            ShapeFile shp = new ShapeFile( fileBaseName.toString(), "rw" );
    783                            shp.writeShape( fcs[i] );
    784                            shp.close();
    785
    786                            // create zip file from .shp, .shx and .dbf
    787                            zipName = shapeBaseName + ".zip";
    788                            shapeFileNames = new String[] { shapeBaseName + ".shp", shapeBaseName + ".shx",
    789                                                           shapeBaseName + ".dbf" };
    790
    791                            // create zip archive
    792                            ZipUtils zu = new ZipUtils();
    793                            zu.doZip( fileDir.toString(), zipName, shapeFileNames, true, false );
    794                            LOG.logDebug( "zip was created: " + zipName );
    795                            files.add( zipName );
    796                        } catch ( Exception e ) {
    797                            LOG.logError( e.getMessage(), e );
    798                            // remove the created shape files
    799                            for ( int j = 0; j < 3; i++ ) {
    800                                File file = new File( fileDir, shapeFileNames[i] );
    801                                file.delete();
    802                            }
    803                            // remove the zip file
    804                            File file = new File( fileDir, zipName );
    805                            file.delete();
    806                            throw new PortalException( Messages.get( "IGEO_STD_CNTXT_ERROR_SAVE_SHAPEFILE", file.getName() ),
    807                                                       e );
    808                        }
    809                    }
    810                }
    811                return files;
    812            }
    813
    814            /**
    815             * @param fc
    816             * @param maxLength
    817             *            (e.g. "127")
    818             * @param postfix
    819             *            (e.g. "...")
    820             */
    821            private void truncatePropertyValues( FeatureCollection fc, int maxLength, String postfix ) {
    822                postfix = postfix == null ? "" : postfix;
    823                StringBuffer truncValue = new StringBuffer();
    824
    825                for ( int i = 0; i < fc.size(); i++ ) {
    826                    Feature feat = fc.getFeature( i );
    827                    FeatureProperty[] fps = feat.getProperties();
    828                    for ( int j = 0; j < fps.length; j++ ) {
    829                        if ( fps[j].getValue() instanceof String ) {
    830                            truncValue.setLength( 0 );
    831                            truncValue.append( (String) fps[j].getValue() );
    832                            if ( truncValue.length() > maxLength ) {
    833                                while ( ( truncValue + postfix ).getBytes().length > maxLength ) {
    834                                    truncValue.setLength( truncValue.length() - 1 );
    835                                }
    836                                fps[j].setValue( truncValue + postfix );
    837                            }
    838                        }
    839                    }
    840                }
    841            }
    842
    843            /**
    844             * stores the passed feature collection as zipped GML and returns the file name
    845             *
    846             * @param gmlDoc
    847             *            the gml feature collection document to be stored
    848             * @param layerName
    849             *            name of the WMS layer corresponding to the feature type requested from the WFS. It is used for
    850             *            file names.
    851             * @param fileDir
    852             *            the name of the download directory, as set in the WMC
    853             * @return
    854             * @throws PortalException
    855             */
    856            private String storeGML( GMLFeatureCollectionDocument gmlDoc, String layerName, File fileDir )
    857                                    throws PortalException {
    858
    859                String zipName = null;
    860                String[] gmlFileNames = null;
    861
    862                try {
    863                    String gmlBaseName = layerName + System.currentTimeMillis() + UUID.randomUUID().toString();
    864
    865                    // create and write gml file
    866                    FileWriter fw = new FileWriter( new File( fileDir, gmlBaseName + ".xml" ) );
    867                    fw.write( gmlDoc.getAsPrettyString() );
    868                    fw.close();
    869
    870                    // create zip file
    871                    zipName = gmlBaseName + ".zip";
    872                    gmlFileNames = new String[] { gmlBaseName + ".xml" };
    873
    874                    // create zip archive
    875                    ZipUtils zu = new ZipUtils();
    876                    zu.doZip( fileDir.toString(), zipName, gmlFileNames, true, false );
    877                    LOG.logDebug( "zip was created: " + zipName );
    878                } catch ( Exception e ) {
    879                    LOG.logError( e.getMessage(), e );
    880                    // remove the created shape files
    881                    for ( int i = 0; i < 3; i++ ) {
    882                        File file = new File( fileDir, gmlFileNames[i] );
    883                        file.delete();
    884                    }
    885                    // remove the zip file
    886                    File file = new File( fileDir, zipName );
    887                    file.delete();
    888                    throw new PortalException( Messages.get( "IGEO_STD_CNTXT_ERROR_SAVE_SHAPEFILE", file.getName() ), e );
    889                }
    890                return zipName;
    891            }
    892
    893            /**
    894             * deletes all files associated to the download order
    895             *
    896             * @param zipFiles
    897             *            list of zip files associated to the download order
    898             */
    899            private void rollback( ArrayList<String> zipFiles, String dir ) {
    900
    901                try {
    902                    for ( int i = 0; i < zipFiles.size(); i++ ) {
    903                        String fn = (String) zipFiles.get( i );
    904                        File file = new File( dir + "/" + fn );
    905                        file.delete();
    906                    }
    907                } catch ( Exception e ) {
    908                    LOG.logError( e.getMessage(), e );
    909                }
    910            }
    911
    912            /**
    913             * sends a mail to inform the administrator and the user about an error that raised performing the data access
    914             */
    915            private void sendErrorMail( Exception e, String addr, String target )
    916                                    throws SendMailException {
    917
    918                String mailAddress = mailAddr;
    919                String mailHost = ContextMessages.getString( "Email.MailHost" );
    920                String mailHostUser = ContextMessages.getString( "Email.MailHostUser" );
    921                String mailHostPassword = ContextMessages.getString( "Email.MailHostPassword" );
    922                String sender = ContextMessages.getString( "Email.Sender" );
    923                String subject = ContextMessages.getString( "Email.Header" );
    924
    925                // create mesage for informing the user
    926                String st = StringTools.stackTraceToString( e.getStackTrace() );
    927                String message = null;
    928                if ( addr == null ) {
    929                    message = Messages.get( "IGEO_STD_CNTXT_ERROR_CREATE_SHAPEFILE", target, st );
    930                } else {
    931                    message = Messages.get( "IGEO_STD_CNTXT_ERROR_PERFORM_GETFEATURE", addr, target, st );
    932                }
    933
    934                // send message to the user
    935                MailMessage mm = new EMailMessage( sender, mailAddress, subject, message );
    936                if ( "!Email.MailHostUser!".equals( mailHostUser ) || "!Email.MailHostPassword!".equals( mailHostPassword ) ) {
    937                    MailHelper.createAndSendMail( mm, mailHost );
    938                } else {
    939                    MailHelper.createAndSendMail( mm, mailHost, mailHostUser, mailHostPassword );
    940                }
    941            }
    942
    943            /**
    944             * sends a mail to the user that the data download succeded and informs him where to download the created files
    945             *
    946             * @param files
    947             *            list of created files (names)
    948             *
    949             * @throws SendMailException
    950             */
    951            private void sendSuccessMail( ArrayList<String> files )
    952                                    throws SendMailException {
    953
    954                String mailAddress = mailAddr;
    955                String mailHost = ContextMessages.getString( "Email.MailHost" );
    956                String mailHostUser = ContextMessages.getString( "Email.MailHostUser" );
    957                String mailHostPassword = ContextMessages.getString( "Email.MailHostPassword" );
    958                String senderMail = ContextMessages.getString( "Email.Sender" );
    959                String emailSubject = ContextMessages.getString( "Email.Header" );
    960
    961                // create mesage for informing the user
    962                String address = NetWorker.url2String( downloadURL );
    963                if ( !address.endsWith( "/" ) ) {
    964                    address += "/";
    965                }
    966                StringBuffer sb = new StringBuffer( Messages.get( "IGEO_STD_CNTXT_INFO_EMAIL_DATA_CREATED" ) );
    967                for ( int i = 0; i < files.size(); i++ ) {
    968                    String file = (String) files.get( i );
    969                    sb.append( address + "download?file=" + file + "\n" );
    970                }
    971
    972                // add info on features with to many results
    973                if ( errorFeatures != null ) {
    974                    sb.append( "\n" ).append( Messages.get( "IGEO_STD_CNTXT_TO_MANY_OBJECTS" ) ).append( "\n" );
    975                    sb.append( errorFeatures );
    976                }
    977
    978                // inform the user to contact the admin for layers that caused a serviceException
    979                if ( errorHitsException != null ) {
    980                    sb.append( "\n\n" ).append( Messages.get( "IGEO_STD_CNTXT_DOWNLOAD_SERVICE_EXCEPTION" ) ).append( "\n" );
    981                    sb.append( errorHitsException );
    982                }
    983
    984                // send message to the user
    985                MailMessage mm = new EMailMessage( senderMail, mailAddress, emailSubject, sb.toString() );
    986                if ( "!Email.MailHostUser!".equals( mailHostUser ) || "!Email.MailHostPassword!".equals( mailHostPassword ) ) {
    987                    // if these variables are not set in the properties file, they are set by the getString() method above.
    988                    MailHelper.createAndSendMail( mm, mailHost );
    989                } else {
    990                    MailHelper.createAndSendMail( mm, mailHost, mailHostUser, mailHostPassword );
    991                }
    992            }
    993        }
    994
    995        private class RequestBean {
    996
    997            String serverLocation;
    998
    999            /**
    1000             * GetFeature request for result_type = hits (returning number of hits). may be null.
    1001             */
    1002            GetFeature hitsRequest;
    1003
    1004            /**
    1005             * GetFeature request for result_type = results
    1006             */
    1007            GetFeature request;
    1008
    1009            String layerName;
    1010
    1011            String layerTitle;
    1012
    1013            /**
    1014             * maximum number of results returned by this WFS server, as specified in the WFS capabilities (<ows:Constraint
    1015             * name="DefaultMaxFeatures">). The default value is taken from init parameters, in case the WFS does not
    1016             * provide the value in its capabilities
    1017             */
    1018            int maxFeatures;
    1019
    1020            /**
    1021             * @param server
    1022             *            this WFS server address
    1023             * @param hitsRequest
    1024             *            GetFeature request for result_type = hits (returning number of hits). may be null.
    1025             * @param request
    1026             *            GetFeature request for result_type = results
    1027             * @param maxRequest
    1028             *            maximum number of results returned by this WFS server, as specified in the WFS capabilities
    1029             *            (<ows:Constraint name="DefaultMaxFeatures">). The default value is taken from init parameters, in
    1030             *            case the WFS does not provide the value in its capabilities.
    1031             * @param layerName
    1032             *            the name (identifier) of the corresponding WMS layer
    1033             * @param layerTitle
    1034             *            the title of the corresponding WMS layer
    1035             */
    1036            RequestBean( String server, GetFeature hitsRequest, GetFeature request, int maxRequest, String layerName,
    1037                         String layerTitle ) {
    1038                this.serverLocation = server;
    1039                this.request = request;
    1040                this.maxFeatures = maxRequest;
    1041                this.hitsRequest = hitsRequest;
    1042                this.layerName = layerName;
    1043                this.layerTitle = layerTitle;
    1044            }
    1045        }
    1046
    1047        /**
    1048         * little helper class to store association between IDs and bounding boxes
    1049         */
    1050        protected class FeatureTemplate {
    1051
    1052            /**
    1053             * WMS Layer name
    1054             */
    1055            private String id = null;
    1056
    1057            /**
    1058             * WMS Layer title
    1059             */
    1060            private String title = null;
    1061
    1062            /**
    1063             * WMS address for this layer
    1064             */
    1065            private String serviceURL = null;
    1066
    1067            /**
    1068             * current/requested bbox
    1069             */
    1070            private Envelope bbox = null;
    1071
    1072            FeatureTemplate( String id, String title, Envelope bbox, String serviceURL ) {
    1073                this.id = id;
    1074                this.bbox = bbox;
    1075                this.title = title;
    1076                this.serviceURL = serviceURL;
    1077            }
    1078
    1079            /**
    1080             * @return id (the WMS layer name)
    1081             */
    1082            public String getId() {
    1083                return id;
    1084            }
    1085
    1086            /**
    1087             * @return Title (the WMS layer title)
    1088             */
    1089            public String getTitle() {
    1090                return title;
    1091            }
    1092
    1093            /**
    1094             * @return Envelope (current bbox)
    1095             */
    1096            public Envelope getEnvelope() {
    1097                return bbox;
    1098            }
    1099
    1100            /**
    1101             * @return ServiceURL (of WFS)
    1102             */
    1103            public String getServerURL() {
    1104                return serviceURL;
    1105            }
    1106        }
    1107    }