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