From: <ale...@us...> - 2006-08-14 17:53:27
|
Revision: 4076 Author: alexmilowski Date: 2006-08-14 10:53:09 -0700 (Mon, 14 Aug 2006) ViewCVS: http://svn.sourceforge.net/exist/?rev=4076&view=rev Log Message: ----------- Added webdav servlet Fixed feed metadata update problems where the title was dropped if there was atom:id/etc. elements. Modified Paths: -------------- trunk/eXist-1.0/src/org/exist/atom/modules/AtomModuleBase.java trunk/eXist-1.0/src/org/exist/atom/modules/AtomProtocol.java Added Paths: ----------- trunk/eXist-1.0/src/org/exist/atom/http/WebDAVServlet.java trunk/eXist-1.0/src/org/exist/atom/util/DateFormatter.java Added: trunk/eXist-1.0/src/org/exist/atom/http/WebDAVServlet.java =================================================================== --- trunk/eXist-1.0/src/org/exist/atom/http/WebDAVServlet.java (rev 0) +++ trunk/eXist-1.0/src/org/exist/atom/http/WebDAVServlet.java 2006-08-14 17:53:09 UTC (rev 4076) @@ -0,0 +1,437 @@ +/* + * eXist Open Source Native XML Database + * Copyright (C) 2001-06 The eXist Project + * http://exist-db.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * $Id: WebDAVServlet.java 2782 2006-02-25 18:55:49Z dizzzz $ + */ +package org.exist.atom.http; + +import java.io.IOException; +import java.util.Date; +import java.util.Enumeration; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.log4j.Logger; +import org.exist.EXistException; +import org.exist.atom.Atom; +import org.exist.atom.modules.AtomProtocol; +import org.exist.atom.util.DOM; +import org.exist.atom.util.DOMDB; +import org.exist.atom.util.DateFormatter; +import org.exist.atom.util.NodeHandler; +import org.exist.collections.Collection; +import org.exist.collections.IndexInfo; +import org.exist.collections.triggers.TriggerException; +import org.exist.dom.DocumentImpl; +import org.exist.dom.ElementImpl; + +import org.exist.http.webdav.WebDAV; +import org.exist.http.webdav.WebDAVMethod; +import org.exist.http.webdav.WebDAVMethodFactory; +import org.exist.http.webdav.methods.Copy; +import org.exist.http.webdav.methods.Delete; +import org.exist.http.webdav.methods.Mkcol; +import org.exist.http.webdav.methods.Move; +import org.exist.http.webdav.methods.Put; +import org.exist.security.PermissionDeniedException; +import org.exist.security.User; +import org.exist.storage.BrokerPool; +import org.exist.storage.DBBroker; +import org.exist.storage.lock.Lock; +import org.exist.storage.txn.TransactionException; +import org.exist.storage.txn.TransactionManager; +import org.exist.storage.txn.Txn; +import org.exist.util.LockException; +import org.exist.util.MimeTable; +import org.exist.util.MimeType; +import org.exist.xmldb.XmldbURI; +import org.exist.xquery.value.StringValue; +import org.safehaus.uuid.UUID; +import org.safehaus.uuid.UUIDGenerator; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * Provides a WebDAV interface that also maintains the atom feed if it exists + * in the directory. + * + * @author wolf + * @author Alex Milowski + */ +public class WebDAVServlet extends HttpServlet { + + protected final static Logger LOG = Logger.getLogger(WebDAVServlet.class); + + class FindEntryByResource implements NodeHandler { + String path; + Element matching; + FindEntryByResource(String path) { + this.path = path; + this.matching = null; + } + public void process(Node parent,Node child) { + Element entry = (Element)child; + NodeList nl = entry.getElementsByTagNameNS(Atom.NAMESPACE_STRING,"content"); + if (nl.getLength()!=0) { + if (path.equals(((Element)nl.item(0)).getAttribute("src"))) { + matching = entry; + } + } + } + + public Element getEntry() { + return matching; + } + + } + + class AtomWebDAVMethodFactory extends WebDAVMethodFactory { + public WebDAVMethod create(String method, BrokerPool pool) { + if (method.equals("PUT")) { + return new AtomPut(pool); + } else if (method.equals("DELETE")) { + return new AtomDelete(pool); + } else if (method.equals("MKCOL")) { + return new AtomMkcol(pool); + } else if (method.equals("MOVE")) { + return new AtomMove(pool); + } else if (method.equals("COPY")) { + return new AtomCopy(pool); + } else { + return super.create(method,pool); + } + } + } + + class AtomPut extends Put { + AtomPut(BrokerPool pool) { + super(pool); + } + public void process(User user, HttpServletRequest request, HttpServletResponse response, XmldbURI path) + throws ServletException, IOException + { + XmldbURI filename = path.lastSegment(); + XmldbURI collUri = path.removeLastSegment(); + DBBroker broker = null; + Collection collection = null; + boolean updateToExisting = false; + try { + broker = pool.get(user); + collection = broker.openCollection(collUri, Lock.READ_LOCK); + updateToExisting = collection.getDocument(broker,filename)!=null; + } catch (EXistException ex) { + throw new ServletException("Exception while getting a broker from the pool.",ex); + } + + super.process(user,request,response,path); + + if (updateToExisting) { + // We do nothing right now + if (collection!=null) { + collection.release(); + } + LOG.debug("Update to existing resource, skipping feed update."); + return; + } + + TransactionManager transact = pool.getTransactionManager(); + Txn transaction = transact.beginTransaction(); + DocumentImpl feedDoc = null; + try { + + LOG.debug("Atom PUT collUri='"+collUri+"'; path="+filename+"';" ); + + if (collection == null || collection.hasChildCollection(filename)) { + // We're already in an error state from the WebDAV action so just return + LOG.debug("No collection or subcollection already exists."); + transact.abort(transaction); + return; + } + + MimeType mime; + String contentType = request.getContentType(); + if (contentType == null) { + mime = MimeTable.getInstance().getContentTypeFor(filename); + if (mime != null) { + contentType = mime.getName(); + } + } else { + int p = contentType.indexOf(';'); + if (p > -1) { + contentType = StringValue.trimWhitespace(contentType.substring(0, p)); + } + mime = MimeTable.getInstance().getContentType(contentType); + } + + if (mime == null) { + mime = MimeType.BINARY_TYPE; + } + + LOG.debug("Acquiring lock on feed document..."); + feedDoc = collection.getDocument(broker,AtomProtocol.FEED_DOCUMENT_URI); + feedDoc.getUpdateLock().acquire(Lock.WRITE_LOCK); + + String title = request.getHeader("Title"); + if (title==null) { + title = filename.toString(); + } + + String created = DateFormatter.toXSDDateTime(new Date()); + ElementImpl feedRoot = (ElementImpl)feedDoc.getDocumentElement(); + DOMDB.replaceTextElement(transaction,feedRoot,Atom.NAMESPACE_STRING,"updated",created,true); + String id = "urn:uuid:"+UUIDGenerator.getInstance().generateRandomBasedUUID(); + Element mediaEntry = AtomProtocol.generateMediaEntry(id,created,title,filename.toString(),mime.getName()); + DOMDB.appendChild(transaction,feedRoot,mediaEntry); + broker.storeXMLResource(transaction, feedDoc); + transact.commit(transaction); + } catch (TransactionException ex) { + transact.abort(transaction); + throw new ServletException("Cannot commit transaction.",ex); + } catch (EXistException ex) { + transact.abort(transaction); + throw new ServletException("Exception while getting a broker from the pool.",ex); + } catch (ParserConfigurationException ex) { + transact.abort(transaction); + throw new ServletException("DOM implementation is misconfigured.",ex); + } catch (LockException ex) { + transact.abort(transaction); + throw new ServletException("Cannot acquire write lock.",ex); + } finally { + if (feedDoc!=null) { + feedDoc.getUpdateLock().release(Lock.WRITE_LOCK); + } + if (collection!=null) { + collection.release(); + } + pool.release(broker); + } + } + } + + class AtomDelete extends Delete { + AtomDelete(BrokerPool pool) { + super(pool); + } + public void process(User user, HttpServletRequest request, HttpServletResponse response, XmldbURI path) + throws ServletException, IOException + { + super.process(user,request,response,path); + TransactionManager transact = pool.getTransactionManager(); + Txn transaction = transact.beginTransaction(); + DocumentImpl feedDoc = null; + DBBroker broker = null; + Collection collection = null; + try { + + broker = pool.get(user); + + XmldbURI filename = path.lastSegment(); + XmldbURI collUri = path.removeLastSegment(); + + LOG.debug("Atom DELETE collUri='"+collUri+"'; path="+filename+"';" ); + + collection = broker.openCollection(collUri, Lock.READ_LOCK); + if (collection == null || collection.hasChildCollection(filename)) { + // We're already in an error state from the WebDAV action so just return + transact.abort(transaction); + return; + } + + feedDoc = collection.getDocument(broker,AtomProtocol.FEED_DOCUMENT_URI); + feedDoc.getUpdateLock().acquire(Lock.WRITE_LOCK); + // Find the entry + FindEntryByResource finder = new FindEntryByResource(filename.toString()); + DOM.findChildren(feedDoc.getDocumentElement(),Atom.NAMESPACE_STRING,"entry",finder); + Element entry = finder.getEntry(); + + if (entry!=null) { + // Remove the entry + ElementImpl feedRoot = (ElementImpl)feedDoc.getDocumentElement(); + feedRoot.removeChild(transaction,entry); + + // Update the feed time + String currentDateTime = DateFormatter.toXSDDateTime(new Date()); + DOMDB.replaceTextElement(transaction,feedRoot,Atom.NAMESPACE_STRING,"updated",currentDateTime,true); + + // Store the change on the feed + LOG.debug("Storing change..."); + broker.storeXMLResource(transaction, feedDoc); + + transact.commit(transaction); + } else { + // the entry is missing, so ignore + transact.abort(transaction); + } + } catch (TransactionException ex) { + transact.abort(transaction); + throw new ServletException("Cannot commit transaction.",ex); + } catch (EXistException ex) { + transact.abort(transaction); + throw new ServletException("Exception while getting a broker from the pool.",ex); + } catch (LockException ex) { + transact.abort(transaction); + throw new ServletException("Cannot acquire write lock.",ex); + } finally { + if (feedDoc!=null) { + feedDoc.getUpdateLock().release(Lock.WRITE_LOCK); + } + if (collection!=null) { + collection.release(); + } + pool.release(broker); + } + + } + } + + class AtomMkcol extends Mkcol { + AtomMkcol(BrokerPool pool) { + super(pool); + } + public void process(User user, HttpServletRequest request, HttpServletResponse response, XmldbURI path) + throws ServletException, IOException + { + DBBroker broker = null; + Collection collection = null; + try { + broker = pool.get(user); + collection = broker.openCollection(path, Lock.READ_LOCK); + if (collection != null) { + collection.release(); + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, + "collection " + request.getPathInfo() + " already exists"); + return; + } + } catch (EXistException ex) { + throw new ServletException("Exception while getting a broker from the pool.",ex); + } + super.process(user,request,response,path); + collection = broker.openCollection(path, Lock.READ_LOCK); + if (collection==null) { + pool.release(broker); + return; + } + DocumentImpl feedDoc = null; + TransactionManager transact = broker.getBrokerPool().getTransactionManager(); + Txn transaction = transact.beginTransaction(); + try { + UUID id = UUIDGenerator.getInstance().generateRandomBasedUUID(); + String currentDateTime = DateFormatter.toXSDDateTime(new Date()); + DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); + docFactory.setNamespaceAware(true); + Document doc = docFactory.newDocumentBuilder().getDOMImplementation().createDocument(Atom.NAMESPACE_STRING,"feed",null); + Element root = doc.getDocumentElement(); + DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"id","urn:uuid:"+id,false); + DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"updated",currentDateTime,false); + DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"title",path.lastSegment().getCollectionPath(),false); + Element editLink = doc.createElementNS(Atom.NAMESPACE_STRING,"link"); + editLink.setAttribute("rel","edit"); + editLink.setAttribute("type",Atom.MIME_TYPE); + editLink.setAttribute("href","#"); + root.appendChild(editLink); + IndexInfo info = collection.validateXMLResource(transaction,broker,AtomProtocol.FEED_DOCUMENT_URI,doc); + collection.store(transaction,broker,info,doc,false); + transact.commit(transaction); + } catch (ParserConfigurationException ex) { + transact.abort(transaction); + throw new ServletException("SAX error: "+ex.getMessage(),ex); + } catch (SAXException ex) { + transact.abort(transaction); + throw new ServletException("SAX error: "+ex.getMessage(),ex); + } catch (TriggerException ex) { + transact.abort(transaction); + throw new ServletException("Trigger failed: "+ex.getMessage(),ex); + } catch (LockException ex) { + transact.abort(transaction); + throw new ServletException("Cannot acquire write lock.",ex); + } catch (PermissionDeniedException ex) { + transact.abort(transaction); + throw new ServletException("Permission denied.",ex); + } catch (EXistException ex) { + transact.abort(transaction); + throw new ServletException("Database exception",ex); + } finally { + collection.release(); + pool.release(broker); + } + } + } + + class AtomMove extends Move { + AtomMove(BrokerPool pool) { + super(pool); + } + } + + class AtomCopy extends Copy { + AtomCopy(BrokerPool pool) { + super(pool); + } + } + + private WebDAV webdav; + /** id of the database registred against the BrokerPool */ + protected String databaseid = BrokerPool.DEFAULT_INSTANCE_NAME; + + + /* (non-Javadoc) + * @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig) + */ + public void init(ServletConfig config) throws ServletException { + super.init(config); + // <fre...@aj...> to allow multi-instance webdav server, + // use a databaseid everywhere + String id = config.getInitParameter("database-id"); + if (id != null && !"".equals(id)) this.databaseid=id; + + int authMethod = WebDAV.DIGEST_AUTH; + String param = config.getInitParameter("authentication"); + + if(param != null && "basic".equalsIgnoreCase(param)) + authMethod = WebDAV.BASIC_AUTH; + + webdav = new WebDAV(authMethod, this.databaseid,new AtomWebDAVMethodFactory()); + } + + /* (non-Javadoc) + * @see javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + dumpHeaders(request); + webdav.process(request, response); + } + + private void dumpHeaders(HttpServletRequest request) { + System.out.println("-------------------------------------------------------"); + System.out.println(request.getMethod()+" "+request.getPathInfo()); + for(Enumeration e = request.getHeaderNames(); e.hasMoreElements(); ) { + String header = (String)e.nextElement(); + System.out.println(header + " = " + request.getHeader(header)); + } + } +} Modified: trunk/eXist-1.0/src/org/exist/atom/modules/AtomModuleBase.java =================================================================== --- trunk/eXist-1.0/src/org/exist/atom/modules/AtomModuleBase.java 2006-08-14 17:50:34 UTC (rev 4075) +++ trunk/eXist-1.0/src/org/exist/atom/modules/AtomModuleBase.java 2006-08-14 17:53:09 UTC (rev 4076) @@ -31,7 +31,6 @@ */ public class AtomModuleBase implements AtomModule { - static DateFormat xsdFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); protected Context context; /** Creates a new instance of AtomModuleBase */ @@ -48,20 +47,6 @@ return context; } - protected String toXSDDateTime(long value) - { - Date d = new Date(value); - return toXSDDateTime(d); - } - - protected String toXSDDateTime(Date d) - { - String result = xsdFormat.format(d); - result = result.substring(0, result.length()-2) - + ":" + result.substring(result.length()-2); - return result; - } - public void process(DBBroker broker,IncomingMessage request,OutgoingMessage response) throws BadRequestException,PermissionDeniedException,NotFoundException,EXistException { Modified: trunk/eXist-1.0/src/org/exist/atom/modules/AtomProtocol.java =================================================================== --- trunk/eXist-1.0/src/org/exist/atom/modules/AtomProtocol.java 2006-08-14 17:50:34 UTC (rev 4075) +++ trunk/eXist-1.0/src/org/exist/atom/modules/AtomProtocol.java 2006-08-14 17:53:09 UTC (rev 4076) @@ -33,6 +33,7 @@ import org.exist.atom.OutgoingMessage; import org.exist.atom.util.DOM; import org.exist.atom.util.DOMDB; +import org.exist.atom.util.DateFormatter; import org.exist.atom.util.NodeHandler; import org.exist.collections.Collection; import org.exist.collections.IndexInfo; @@ -69,8 +70,8 @@ public class AtomProtocol extends AtomFeeds implements Atom { protected final static Logger LOG = Logger.getLogger(AtomProtocol.class); - static final String FEED_DOCUMENT_NAME = ".feed.atom"; - static final XmldbURI FEED_DOCUMENT_URI = XmldbURI.create(FEED_DOCUMENT_NAME); + public static final String FEED_DOCUMENT_NAME = ".feed.atom"; + public static final XmldbURI FEED_DOCUMENT_URI = XmldbURI.create(FEED_DOCUMENT_NAME); class FindEntry implements NodeHandler { String id; @@ -160,7 +161,7 @@ TransactionManager transact = broker.getBrokerPool().getTransactionManager(); Txn transaction = transact.beginTransaction(); String id = "urn:uuid:"+UUIDGenerator.getInstance().generateRandomBasedUUID(); - String currentDateTime = toXSDDateTime(new Date()); + String currentDateTime = DateFormatter.toXSDDateTime(new Date()); Element publishedE = DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"published",currentDateTime,true,true); DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"updated",currentDateTime,true,true); DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"id",id,true,true); @@ -239,17 +240,18 @@ broker.saveCollection(transaction, collection); } UUID id = UUIDGenerator.getInstance().generateRandomBasedUUID(); - String currentDateTime = toXSDDateTime(new Date()); + String currentDateTime = DateFormatter.toXSDDateTime(new Date()); DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"updated",currentDateTime,true); DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"id","urn:uuid:"+id,true); Element editLink = findLink(root,"edit"); if (editLink!=null) { - throw new BadRequestException("An edit link relation cannot be specified in the entry."); + throw new BadRequestException("An edit link relation cannot be specified in the feed."); } editLink = doc.createElementNS(Atom.NAMESPACE_STRING,"link"); editLink.setAttribute("rel","edit"); editLink.setAttribute("type",Atom.MIME_TYPE); editLink.setAttribute("href","#"); + root.appendChild(editLink); IndexInfo info = collection.validateXMLResource(transaction,broker,FEED_DOCUMENT_URI,doc); collection.store(transaction,broker,info,doc,false); transact.commit(transaction); @@ -272,6 +274,10 @@ if (collection == null) { throw new BadRequestException("Collection "+request.getPath()+" does not exist."); } + DocumentImpl feedDoc = collection.getDocument(broker,FEED_DOCUMENT_URI); + if (feedDoc==null) { + throw new BadRequestException("Feed at "+request.getPath()+" does not exist."); + } String filename = request.getHeader("Slug"); if (filename==null) { String ext = MimeTable.getInstance().getPreferredExtension(mime); @@ -308,16 +314,14 @@ collection.addBinaryResource(transaction, broker, docUri, is, contentType, (int) tempFile.length()); is.close(); } - DocumentImpl feedDoc = null; try { LOG.debug("Acquiring lock on feed document..."); - feedDoc = collection.getDocument(broker,FEED_DOCUMENT_URI); feedDoc.getUpdateLock().acquire(Lock.WRITE_LOCK); String title = request.getHeader("Title"); if (title==null) { title = filename; } - String created = toXSDDateTime(new Date()); + String created = DateFormatter.toXSDDateTime(new Date()); ElementImpl feedRoot = (ElementImpl)feedDoc.getDocumentElement(); DOMDB.replaceTextElement(transaction,feedRoot,Atom.NAMESPACE_STRING,"updated",created,true); String id = "urn:uuid:"+UUIDGenerator.getInstance().generateRandomBasedUUID(); @@ -443,7 +447,7 @@ ElementImpl feedRoot = (ElementImpl)feedDoc.getDocumentElement(); // Modify the feed by merging the new feed-level elements - mergeFeed(transaction,feedRoot,root,toXSDDateTime(new Date())); + mergeFeed(transaction,feedRoot,root,DateFormatter.toXSDDateTime(new Date())); // Store the feed broker.storeXMLResource(transaction, feedDoc); @@ -470,7 +474,7 @@ DocumentImpl feedDoc = null; TransactionManager transact = broker.getBrokerPool().getTransactionManager(); Txn transaction = transact.beginTransaction(); - String currentDateTime = toXSDDateTime(new Date()); + String currentDateTime = DateFormatter.toXSDDateTime(new Date()); try { // Get the feed LOG.debug("Acquiring lock on feed document..."); @@ -604,7 +608,7 @@ DocumentImpl feedDoc = null; TransactionManager transact = broker.getBrokerPool().getTransactionManager(); Txn transaction = transact.beginTransaction(); - String currentDateTime = toXSDDateTime(new Date()); + String currentDateTime = DateFormatter.toXSDDateTime(new Date()); try { // Get the feed @@ -780,13 +784,13 @@ if (lname.equals("updated") || lname.equals("published") || lname.equals("id")) { - return; + continue; } // Skip the edit link relations if (lname.equals("link")) { String rel = ((Element)child).getAttribute("rel"); - if (!rel.equals("edit")) { - return; + if (rel.equals("edit")) { + continue; } } } @@ -814,7 +818,7 @@ return null; } - protected Element generateMediaEntry(String id,String created,String title,String filename,String mimeType) + public static Element generateMediaEntry(String id,String created,String title,String filename,String mimeType) throws ParserConfigurationException { Added: trunk/eXist-1.0/src/org/exist/atom/util/DateFormatter.java =================================================================== --- trunk/eXist-1.0/src/org/exist/atom/util/DateFormatter.java (rev 0) +++ trunk/eXist-1.0/src/org/exist/atom/util/DateFormatter.java 2006-08-14 17:53:09 UTC (rev 4076) @@ -0,0 +1,41 @@ +/* + * Date.java + * + * Created on July 6, 2006, 9:30 PM + * + * (C) R. Alexander Milowski al...@mi... + */ + +package org.exist.atom.util; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * + * @author R. Alexander Milowski + */ +public class DateFormatter { + + /** Creates a new instance of Date */ + private DateFormatter() { + } + + static DateFormat xsdFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + + public static String toXSDDateTime(long value) + { + Date d = new Date(value); + return toXSDDateTime(d); + } + + public static String toXSDDateTime(Date d) + { + String result = xsdFormat.format(d); + result = result.substring(0, result.length()-2) + + ":" + result.substring(result.length()-2); + return result; + } + +} This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |